diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 004add68de..2f6f88d0df 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,11 +1,4 @@ -# [Choice] PHP version: 7, 7.4, 7.3 -ARG VARIANT=7 -FROM mcr.microsoft.com/vscode/devcontainers/php:${VARIANT} - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +FROM mcr.microsoft.com/vscode/devcontainers/php:8.1 # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ @@ -17,11 +10,4 @@ RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/shar # PHP memory limit RUN echo "memory_limit=768M" > /usr/local/etc/php/php.ini -# Composer v2 -RUN EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" \ - && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ - && ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \ - && if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then >&2 echo 'ERROR: Invalid installer checksum'; rm composer-setup.php; exit 1; fi \ - && php composer-setup.php --version=2.0.0-RC1 \ - && php -r "unlink('composer-setup.php');" \ - && mv composer.phar /usr/local/bin/composer +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer diff --git a/.devcontainer/base.Dockerfile b/.devcontainer/base.Dockerfile deleted file mode 100644 index 5c57924435..0000000000 --- a/.devcontainer/base.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# [Choice] PHP version: 7, 7.4, 7.3 -ARG VARIANT=7 -FROM php:${VARIANT}-apache - -# [Option] Install zsh -ARG INSTALL_ZSH="true" -# [Option] Upgrade OS packages to their latest versions -ARG UPGRADE_PACKAGES="true" - -# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID -COPY library-scripts/common-debian.sh /tmp/library-scripts/ -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ - && apt-get -y install --no-install-recommends lynx \ - && usermod -aG www-data ${USERNAME} \ - && sed -i -e "s/Listen 80/Listen 80\\nListen 8080/g" /etc/apache2/ports.conf \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts - -# Install xdebug -RUN yes | pecl install xdebug \ - && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini \ - && echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/xdebug.ini \ - && echo "xdebug.remote_autostart=on" >> /usr/local/etc/php/conf.d/xdebug.ini \ - && rm -rf /tmp/pear - -# Install composer -RUN curl -sSL https://getcomposer.org/installer | php \ - && chmod +x composer.phar \ - && mv composer.phar /usr/local/bin/composer - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="none" -ENV NVM_DIR=/usr/local/share/nvm -ENV NVM_SYMLINK_CURRENT=true \ - PATH=${NVM_DIR}/current/bin:${PATH} -COPY library-scripts/node-debian.sh /tmp/library-scripts/ -RUN if [ "$INSTALL_NODE" = "true" ]; then /bin/bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; fi \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts - -# [Optional] Uncomment this section to install additional packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e2f0ccb650..ff13dd64a3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,7 @@ { "name": "PHP", "build": { - "dockerfile": "Dockerfile", - "args": { - // Update VARIANT to pick a PHP version: 7, 7.4, 7.3 - "VARIANT": "7.4", - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*" - } + "dockerfile": "Dockerfile" }, // Set *default* container specific settings.json values on container create. diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh index f35e77fe1b..d230a14e82 100644 --- a/.devcontainer/library-scripts/node-debian.sh +++ b/.devcontainer/library-scripts/node-debian.sh @@ -18,7 +18,7 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -# Treat a user name of "none" or non-existant user as root +# Treat a user name of "none" or non-existent user as root if [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then USERNAME=root fi diff --git a/.editorconfig b/.editorconfig index 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/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d04f75155f..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: -- package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: composer - directory: "/compiler" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..9657e99a52 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,71 @@ +{ + "extends": [ + "config:recommended", + "schedule:weekly" + ], + "dependencyDashboard": true, + "rangeStrategy": "update-lockfile", + "rebaseWhen": "conflicted", + "baseBranches": [ + "2.1.x" + ], + "packageRules": [ + { + "enabled": false, + "matchPackageNames": [ + "*" + ] + }, + { + "matchFileNames": [ + "+(composer.json)" + ], + "enabled": true, + "matchBaseBranches": [ + "2.1.x" + ] + }, + { + "matchFileNames": [ + "apigen/**" + ], + "enabled": true, + "groupName": "apigen" + }, + { + "matchFileNames": [ + "issue-bot/**" + ], + "enabled": true, + "groupName": "issue-bot" + }, + { + "matchFileNames": [ + "changelog-generator/**" + ], + "enabled": true, + "groupName": "changelog-generator" + }, + { + "matchFileNames": [ + "compiler/**" + ], + "enabled": true, + "groupName": "compiler" + }, + { + "matchFileNames": [ + "tests/composer.json" + ], + "enabled": true, + "groupName": "paratest" + }, + { + "matchFileNames": [ + ".github/**" + ], + "enabled": true, + "groupName": "github-actions" + } + ] +} diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore new file mode 100644 index 0000000000..f06235c460 --- /dev/null +++ b/.github/scripts/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.github/scripts/diffPrefixes.php b/.github/scripts/diffPrefixes.php new file mode 100644 index 0000000000..0f3c22cbfc --- /dev/null +++ b/.github/scripts/diffPrefixes.php @@ -0,0 +1,85 @@ +diff(file_get_contents($oldFilePath), file_get_contents($newFilePath)); + if ($stringDiff === '') { + continue; + } + + $isDifferent = true; + + echo "$path:\n"; + $startLine = 1; + $startContext = 1; + foreach (explode("\n", $stringDiff) as $i => $line) { + $matches = Strings::match($line, '/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/'); + if ($matches !== null) { + $startLine = (int) $matches[1]; + $startContext = (int) $matches[2]; + continue; + } + + if ($lineNumber < $startLine || $lineNumber > ($startLine + $startContext)) { + continue; + } + + if (str_starts_with($line, '+')) { + echo "\033[32m$line\033[0m\n"; + } elseif (str_starts_with($line, '-')) { + echo "\033[31m$line\033[0m\n"; + } else { + echo "$line\n"; + } + } + + echo "\n"; +} + +if ($isDifferent) { + exit(1); +} diff --git a/.github/scripts/find-artifact.ts b/.github/scripts/find-artifact.ts new file mode 100644 index 0000000000..3f3524c9cc --- /dev/null +++ b/.github/scripts/find-artifact.ts @@ -0,0 +1,63 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; + +interface Inputs { + github: ReturnType; + context: typeof github.context; + core: typeof core; +} + +module.exports = async ({github, context, core}: Inputs) => { + const commitSha = process.env.BASE_SHA; + const artifactName = process.env.ARTIFACT_NAME; + const workflowName = process.env.WORKFLOW_NAME; + + // Get all workflow runs for this commit + const runs = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 20, + event: "push", + head_sha: commitSha + }); + + if (runs.data.workflow_runs.length === 0) { + core.setFailed(`No workflow runs found for commit ${commitSha}`); + return; + } + + const workflowRuns = runs.data.workflow_runs; + if (workflowRuns.length === 0) { + core.setFailed(`No workflow runs found for commit ${commitSha}`); + return; + } + + let found = false; + for (const run of workflowRuns) { + if (run.status !== "completed" || run.conclusion !== "success") { + continue; + } + + if (run.name !== workflowName) { + continue; + } + + const artifactsResp = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + + const artifact = artifactsResp.data.artifacts.find(a => a.name === artifactName); + if (artifact) { + core.setOutput("artifact_id", artifact.id.toString()); + core.setOutput("run_id", run.id.toString()); + found = true; + break; + } + } + + if (!found) { + core.setFailed(`No artifact named '${artifactName}' found for commit ${commitSha}`); + } +} diff --git a/.github/scripts/listPrefix.php b/.github/scripts/listPrefix.php new file mode 100644 index 0000000000..d7f902e855 --- /dev/null +++ b/.github/scripts/listPrefix.php @@ -0,0 +1,32 @@ +setFlags(RecursiveDirectoryIterator::SKIP_DOTS); +$files = new RecursiveIteratorIterator($iterator); + +$locations = []; +foreach ($files as $file) { + $path = $file->getPathname(); + if ($file->getExtension() !== 'php') { + continue; + } + $contents = file_get_contents($path); + $lines = explode("\n", $contents); + foreach ($lines as $i => $line) { + if (!str_contains($line, '_PHPStan_checksum')) { + continue; + } + + $trimmedPath = substr($path, strlen($dir) + 1); + if (str_starts_with($trimmedPath, 'vendor/composer/autoload_')) { + continue; + } + $locations[] = $trimmedPath . ':' . ($i + 1); + } +} +sort($locations); +echo implode("\n", $locations); +echo "\n"; diff --git a/.github/scripts/package-lock.json b/.github/scripts/package-lock.json new file mode 100644 index 0000000000..be699a73ce --- /dev/null +++ b/.github/scripts/package-lock.json @@ -0,0 +1,323 @@ +{ + "name": "scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scripts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.1" + }, + "devDependencies": { + "@types/node": "^22.15.29", + "typescript": "^5.8.3" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 0000000000..e809b2ee73 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,20 @@ +{ + "name": "scripts", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.1" + }, + "devDependencies": { + "@types/node": "^22.15.29", + "typescript": "^5.8.3" + } +} diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json new file mode 100644 index 0000000000..62ac6dae38 --- /dev/null +++ b/.github/scripts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["find-artifact.ts"] +} diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml new file mode 100644 index 0000000000..8847e326dc --- /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.2" + + - uses: "ramsey/composer-install@v3" + + - name: "Install ApiGen dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "apigen" + + - 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/${{ github.ref_name }}/index.html' + resolve_before_filtering: 'true' + verbose: 'warning' + max_retry_time: 30 + max_retries: 5 diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index fd0c7f37a4..53f74a4996 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,21 +6,25 @@ on: pull_request: push: branches: - - "master" + - "2.1.x" + paths: + - 'src/**' + - '.github/workflows/backward-compatibility.yml' -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" +concurrency: + group: bc-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: backward-compatibility: name: "Backward Compatibility" runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -28,16 +32,15 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.2" - - name: "Install dependencies" - run: "composer install --no-dev --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - name: "Install BackwardCompatibilityCheck" run: | composer global config minimum-stability dev composer global config prefer-stable true - composer global require --dev ondrejmirtes/backward-compatibility-check:^5.0.7 + composer global require --dev ondrejmirtes/backward-compatibility-check:^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..4cb6286c2b --- /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" + + - uses: "ramsey/composer-install@v3" + + - name: "Install issue-bot dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "issue-bot" + + - name: "Tests" + working-directory: "issue-bot" + run: ${{ matrix.script }} diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml new file mode 100644 index 0000000000..1dfc0d775c --- /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.2" + + - uses: "ramsey/composer-install@v3" + + - name: "Install Changelog Generator dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "changelog-generator" + + - name: "PHPStan" + working-directory: "changelog-generator" + run: "../bin/phpstan" diff --git a/.github/workflows/compiler-tests.yml b/.github/workflows/compiler-tests.yml deleted file mode 100644 index ca0ffc885a..0000000000 --- a/.github/workflows/compiler-tests.yml +++ /dev/null @@ -1,49 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "Compiler tests" - -on: - pull_request: - push: - branches: - - "master" - -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" - -jobs: - compiler-tests: - name: "Compiler Tests" - - runs-on: "ubuntu-latest" - timeout-minutes: 30 - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.0" - - - name: "Install dependencies" - run: "composer install --no-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 list - - - uses: actions/upload-artifact@v2 - with: - name: phpstan.phar - path: tmp/phpstan.phar diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 0000000000..a853501487 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c7217ff0c5..f6a3e86255 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -4,78 +4,416 @@ name: "E2E Tests" on: pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "master" + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" +concurrency: + group: e2e-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: result-cache-e2e-tests: name: "Result cache E2E tests" - - runs-on: ${{ matrix.operating-system }} - timeout-minutes: 30 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php-version: - - "7.4" - operating-system: [ubuntu-latest, windows-latest] + include: + - script: | + cd e2e/result-cache-1 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-2 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-3 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-4 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-5 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-6 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-7 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug10449 + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug10449b + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug-9622 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug-9622-trait + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/env-parameter + export PHPSTAN_RESULT_CACHE_PATH=/some/path + ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.resultCachePath') + [[ "$ACTUAL" == "/some/path" ]]; + - script: | + cd e2e/result-cache-8 + composer install + ../../bin/phpstan + echo -en '\n' >> build/CustomRule.php + OUTPUT=$(../../bin/phpstan analyze 2>&1 || true) + echo "$OUTPUT" + ../bashunit -a contains 'Result cache might not behave correctly' "$OUTPUT" + ../bashunit -a contains 'ResultCache8E2E\CustomRule' "$OUTPUT" + - script: | + cd e2e/env-int-key + env 1=1 ../../bin/phpstan analyse test.php + - script: | + cd e2e/result-cache-scanned + ../../bin/phpstan + patch -b src/Generated/Foo.php < patch.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'Result cache not used because the metadata do not match: projectConfig, scannedFiles' "$OUTPUT" + ../bashunit -a contains 'Instantiated class ResultCacheE2EGenerated\Foo not found.' "$OUTPUT" + - script: | + cd e2e/result-cache-traits + ../../bin/phpstan analyse + patch -b src/FooTrait.php < renameFooTraitMethod.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'ClassMentioningClassUsingBarTrait.php:10:Call to an undefined method ResultCacheE2ETraits\ClassUsingBarTrait::doFooTrait(). [identifier=method.notFound]' "$OUTPUT" + - script: | + cd e2e/editor-mode + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'Bar.php:10:Parameter #1 $s of method EditorModeE2E\Bar::requireString() expects string, int given. [identifier=argument.type]' "$OUTPUT" + ../bashunit -a contains 'Foo.php:10:Method EditorModeE2E\Foo::doFoo() should return int but returns string. [identifier=return.type]' "$OUTPUT" + ../bashunit -a contains 'Result cache is saved.' "$OUTPUT" + + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw --tmp-file differentFoo.php --instead-of src/Foo.php") + echo "$OUTPUT" + ../bashunit -a contains 'Foo.php:10:Method EditorModeE2E\Foo::doFoo() should return float but returns string. [identifier=return.type]' "$OUTPUT" + ../bashunit -a not_contains 'Foo.php:10:Method EditorModeE2E\Foo::doFoo() should return int but returns string. [identifier=return.type]' "$OUTPUT" + ../bashunit -a not_contains 'differentFoo.php' "$OUTPUT" + ../bashunit -a contains 'Bar.php:10:Parameter #1 $s of method EditorModeE2E\Bar::requireString() expects string, float given. [identifier=argument.type]' "$OUTPUT" + ../bashunit -a contains 'Result cache restored. 2 files will be reanalysed.' "$OUTPUT" + ../bashunit -a contains 'Result cache was not saved because of --tmp-file and --instead-of CLI options passed (editor mode).' "$OUTPUT" + - 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 + - script: | + cd e2e/bug-10483 + ../../bin/phpstan steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.2" extensions: mbstring ini-values: memory_limit=256M - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - - name: "Tests" - run: | - git clone https://github.com/nikic/PHP-Parser.git tests/e2e/PHP-Parser && git -C tests/e2e/PHP-Parser checkout v3.1.5 && composer install --working-dir tests/e2e/PHP-Parser && vendor/bin/phpunit tests/e2e/ResultCacheEndToEndTest.php + - name: "Patch PHPStan" + run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" + + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.0" + + - name: "Test" + run: "${{ matrix.script }}" e2e-tests: name: "E2E tests" runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 strategy: + fail-fast: false matrix: include: - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php tests/e2e/data/timecop.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php -c tests/e2e/data/empty.neon tests/e2e/data/timecop.php" tools: "pecl" extensions: "timecop-beta" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "soap" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "" - script: "bin/phpstan analyse -l 8 tests/e2e/anon-class/Granularity.php" extensions: "" + - script: "bin/phpstan analyse -l 8 e2e/phpstan-phpunit-190/test.php -c e2e/phpstan-phpunit-190/test.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src -c e2e/only-files-not-analysed-trait/ignore.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src/Foo.php e2e/only-files-not-analysed-trait/src/BarTrait.php -c e2e/only-files-not-analysed-trait/no-ignore.neon" + extensions: "" + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --debug --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --debug --configuration test.neon + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --configuration test.neon + - script: | + cd e2e/discussion-11362 + composer install + ../../bin/phpstan + - script: | + cd e2e/bug-11819 + ../../bin/phpstan + - script: | + cd e2e/composer-and-phpstan-version-config + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-open-end-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v5 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v7 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-no-versions + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config-invalid + OUTPUT=$(../bashunit -a exit_code "1" ../../bin/phpstan) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid configuration' "$OUTPUT" + ../bashunit -a contains 'Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.' "$OUTPUT" + - script: | + cd e2e/composer-version-config-patch + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/bug13425 + timeout 15 ../bashunit -a exit_code "1" "../../bin/phpstan analyze src/ plugins/" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "7.4" + php-version: "8.2" tools: ${{ matrix.tools }} extensions: ${{ matrix.extensions }} - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" + + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.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..19055acdc7 --- /dev/null +++ b/.github/workflows/issue-bot.yml @@ -0,0 +1,196 @@ +# 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" + uses: "ramsey/composer-install@v3" + with: + working-directory: "issue-bot" + + - 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" + + - uses: "ramsey/composer-install@v3" + with: + composer-options: "--no-dev" + + - name: "Install issue-bot dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "issue-bot" + + - 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" + uses: "ramsey/composer-install@v3" + with: + working-directory: "issue-bot" + + - 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: | + set +e + ./console.php evaluate >> $GITHUB_STEP_SUMMARY + exit_code="$?" + + if [[ "$exit_code" == "2" ]]; then + echo "::notice file=.github/workflows/issue-bot.yml,line=3 ::Issue bot detected open issues which are affected by this pull request - see https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + exit 0 + fi + + exit $exit_code + + - 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: | + set +e + ./console.php evaluate --post-comments >> $GITHUB_STEP_SUMMARY + exit_code="$?" + + # its fine when issue-bot found affected issues + if [[ "$exit_code" == "2" ]]; then + exit 0 + fi + + exit $exit_code diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01976ec12f..f9c451b89d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,30 +6,33 @@ on: pull_request: push: branches: - - "master" + - "2.1.x" -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" +concurrency: + group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: lint: name: "Lint" runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -37,15 +40,35 @@ jobs: coverage: "none" php-version: "${{ matrix.php-version }}" - - name: "Validate Composer" - run: "composer validate" + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 --update-with-dependencies --ignore-platform-reqs" + + - uses: "ramsey/composer-install@v3" - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Change to simple-downgrade PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.4" - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + run: | + composer install --no-interaction --no-progress --working-dir=compiler + ./compiler/vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }} + composer dump + + - name: "Re-store PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Validate Composer" + run: "composer validate" - name: "Lint" run: "make lint" @@ -54,28 +77,34 @@ jobs: name: "Coding Standard" runs-on: "ubuntu-latest" - timeout-minutes: 30 - - strategy: - matrix: - php-version: - - "8.0" + timeout-minutes: 60 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 + + - name: "Checkout build-cs" + uses: actions/checkout@v4 + with: + repository: "phpstan/build-cs" + path: "build-cs" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" + + - name: "Install build-cs dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "build-cs" - name: "Lint" run: "make lint" @@ -87,25 +116,40 @@ jobs: name: "Dependency Analysis" runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 - strategy: - matrix: - php-version: - - "8.0" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - uses: "ramsey/composer-install@v3" + + - name: "Composer Dependency Analyser" + run: "make composer-dependency-analyser" + + name-collision: + name: "Name Collision Detector" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.5" - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - - name: "Composer Require Checker" - run: "make composer-require-checker" + - name: "Name Collision Detector" + run: "make name-collision" diff --git a/.github/workflows/merge-bot-pr.yml b/.github/workflows/merge-bot-pr.yml 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-old.yml b/.github/workflows/phar-old.yml deleted file mode 100644 index eb91eb3d60..0000000000 --- a/.github/workflows/phar-old.yml +++ /dev/null @@ -1,89 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "Compile PHAR" - -on: - push: - tags: - - '0.12.*' - -jobs: - compile: - name: "Compile PHAR" - runs-on: "ubuntu-latest" - timeout-minutes: 30 - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - with: - fetch-depth: 0 - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.0" - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" - - - name: "Install compiler dependencies" - run: "composer install --no-interaction --no-progress --no-suggest --working-dir=compiler" - - - name: "Transform source code" - run: php bin/transform-source.php - - - name: "Compile PHAR" - run: php compiler/bin/compile - - - name: "Configure GPG signing key" - run: echo "$GPG_SIGNING_KEY" | base64 --decode | gpg --import --no-tty --batch --yes - env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - - - name: "Get Git log" - id: git-log - run: echo ::set-output name=log::$(git log ${{ github.event.before }}..${{ github.event.after }} --reverse --pretty='%H %s' | sed -e 's/^/https:\/\/github.com\/phpstan\/phpstan-src\/commit\//') - - - name: "Checkout phpstan-dist" - uses: "actions/checkout@v2" - with: - repository: phpstan/phpstan - ref: 0.12.x - path: phpstan-dist - token: ${{ secrets.PAT }} - - - name: "cp PHAR" - run: cp tmp/phpstan.phar phpstan-dist/phpstan.phar - - - name: "Sign PHAR" - working-directory: phpstan-dist - run: rm phpstan.phar.asc && gpg --command-fd 0 --pinentry-mode loopback -u "$GPG_ID" --batch --detach-sign --armor --output phpstan.phar.asc phpstan.phar - env: - GPG_ID: ${{ secrets.GPG_ID }} - - - name: "Verify PHAR" - working-directory: phpstan-dist - run: "gpg --verify phpstan.phar.asc" - - - name: "Set Git signing key" - working-directory: phpstan-dist - run: git config user.signingkey "$GPG_ID" - env: - GPG_ID: ${{ secrets.GPG_ID }} - - - name: "Configure Git" - working-directory: phpstan-dist - run: | - git config user.email "ondrej@mirtes.cz" && \ - git config user.name "Ondrej Mirtes" - - - name: "Commit PHAR - tag" - working-directory: phpstan-dist - run: | - git add phpstan.phar phpstan.phar.asc && \ - git commit -S -m "PHPStan ${GITHUB_REF#refs/tags/}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin 0.12.x && \ - git tag -s ${GITHUB_REF#refs/tags/} -m "${GITHUB_REF#refs/tags/}" && \ - git push --quiet origin ${GITHUB_REF#refs/tags/} diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index a0de139f4d..6485a87369 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -3,21 +3,31 @@ name: "Compile PHAR" on: + pull_request: push: branches: - - "master" + - "2.1.x" tags: - - '1.*' + - '2.1.*' + +concurrency: + group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests + cancel-in-progress: true jobs: - compile: - name: "Compile PHAR" + compiler-tests: + name: "Compiler Tests" + runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 + + outputs: + checksum: ${{ steps.checksum.outputs.md5 }} + compiler_changed: ${{ steps.changes.outputs.compiler }} steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -25,75 +35,361 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.2" + extensions: mbstring, intl + + - uses: "ramsey/composer-install@v3" - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + # only sebastian/diff ^4 supports PHP 7.4 so we need that in the PHAR + - name: "Downgrade PHPUnit" + run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 --update-with-dependencies --ignore-platform-reqs" - name: "Install compiler dependencies" - run: "composer install --no-interaction --no-progress --no-suggest --working-dir=compiler" + uses: "ramsey/composer-install@v3" + with: + working-directory: "compiler" + + - name: "Compiler tests" + working-directory: "compiler" + run: "vendor/bin/phpunit -c tests/phpunit.xml tests" + + - name: "Compiler PHPStan" + working-directory: "compiler" + run: "vendor/bin/phpstan analyse -l 8 src tests" - - name: "Transform source code" - run: php bin/transform-source.php + - name: "Prepare for PHAR compilation" + working-directory: "compiler" + run: "php bin/prepare" + + - name: "Dump autoloader one more time for attributes" + run: "composer dump" + + - name: "Install Box dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "compiler/box" - name: "Compile PHAR" - run: php compiler/bin/compile + working-directory: "compiler/build" + run: "php ../box/vendor/bin/box 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: "Configure GPG signing key" - run: echo "$GPG_SIGNING_KEY" | base64 --decode | gpg --import --no-tty --batch --yes + - name: "Delete PHAR" + run: "rm tmp/phpstan.phar" + + - name: "Set autoloader suffix" + run: "composer config autoloader-suffix PHPStanChecksum" + + - uses: "ramsey/composer-install@v3" env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + COMPOSER_ROOT_VERSION: "2.1.x-dev" - - name: "Get Git log" - id: git-log - run: echo ::set-output name=log::$(git log ${{ github.event.before }}..${{ github.event.after }} --reverse --pretty='%H %s' | sed -e 's/^/https:\/\/github.com\/phpstan\/phpstan-src\/commit\//') + - name: "Compile PHAR for checksum" + working-directory: "compiler/build" + run: "php ../box/vendor/bin/box 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" + + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + compiler: + - 'compiler/**' + - '.github/workflows/phar.yml' + - '.github/scripts/**' + + 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}} + + download-base-sha-phar: + name: "Download base SHA PHAR" + needs: compiler-tests + if: github.event_name == 'pull_request' && needs.compiler-tests.outputs.compiler_changed == 'true' + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + + - name: Get base commit SHA + id: base + run: echo "base_sha=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + working-directory: .github/scripts + run: npm ci + + - name: "Compile TS scripts" + working-directory: .github/scripts + run: npx tsc + + - name: Find phar-file-checksum from base commit + id: find-artifact + uses: actions/github-script@v7 + env: + BASE_SHA: ${{ steps.base.outputs.base_sha }} + ARTIFACT_NAME: phar-file-checksum + WORKFLOW_NAME: Compile PHAR + with: + script: | + const script = require('./.github/scripts/dist/find-artifact.js'); + await script({github, context, core}) + + # saved to phar-file-checksum/phpstan.phar + - name: Download old artifact by ID + uses: actions/download-artifact@v4 + with: + artifact-ids: ${{ steps.find-artifact.outputs.artifact_id }} + run-id: ${{ steps.find-artifact.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: "Upload old artifact" + uses: actions/upload-artifact@v4 + with: + name: phar-file-checksum-base + path: phar-file-checksum/phpstan.phar + + checksum-phar: + name: "Checksum PHAR" + needs: + - compiler-tests + - download-base-sha-phar + runs-on: "ubuntu-latest" + steps: + # saved to phpstan.phar + - name: "Download base phpstan.phar" + uses: actions/download-artifact@v4 + with: + name: phar-file-checksum-base + + - name: "Save old checksum" + id: "old_checksum" + run: echo "md5=$(md5sum phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: "Assert checksum" + run: | + old_checksum=${{ steps.old_checksum.outputs.md5 }} + new_checksum=${{needs.compiler-tests.outputs.checksum}} + [[ "$old_checksum" == "$new_checksum" ]]; + + phar-prefix-diff: + name: "PHAR Prefix Diff" + needs: download-base-sha-phar + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + + # saved to phar-file-checksum/phpstan.phar + - name: "Download phpstan.phar" + uses: actions/download-artifact@v4 + with: + name: phar-file-checksum + path: phar-file-checksum + + # saved to phar-file-checksum-base/phpstan.phar + - name: "Download base phpstan.phar" + uses: actions/download-artifact@v4 + with: + name: phar-file-checksum-base + path: phar-file-checksum-base + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.2" + + - uses: "ramsey/composer-install@v3" + + - name: "Install Box dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "compiler/box" + + - name: "Extract old phpstan.phar" + run: "php compiler/box/vendor/bin/box extract phar-file-checksum-base/phpstan.phar phar-old" + + - name: "Extract new phpstan.phar" + run: "php compiler/box/vendor/bin/box extract phar-file-checksum/phpstan.phar phar-new" + + - name: "List prefix locations in old PHAR" + run: "php .github/scripts/listPrefix.php ${{ github.workspace }}/phar-old > phar-old.txt" + + - name: "List prefix locations in new PHAR" + run: "php .github/scripts/listPrefix.php ${{ github.workspace }}/phar-new > phar-new.txt" + + - name: "Diff locations" + run: "diff -u phar-old.txt phar-new.txt > diff.txt || true" + + - name: "Diff files where prefix changed" + run: "php .github/scripts/diffPrefixes.php ${{ github.workspace }}/diff.txt ${{ github.workspace }}/phar-old ${{ github.workspace }}/phar-new" + + commit: + name: "Commit PHAR" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.1.x' || startsWith(github.ref, 'refs/tags/'))" + needs: compiler-tests + runs-on: "ubuntu-latest" + timeout-minutes: 60 + steps: + - + name: Import GPG key + id: import-gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PHPSTANBOT_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PHPSTANBOT_KEY_PASSPHRASE }} + git_config_global: true + git_user_signingkey: true + git_commit_gpgsign: true - name: "Checkout phpstan-dist" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: repository: phpstan/phpstan path: phpstan-dist - token: ${{ secrets.PAT }} + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + ref: 2.1.x + + - name: "Get previous pushed dist commit" + id: previous-commit + working-directory: phpstan-dist + run: echo "sha=$(sed -n '2p' .phar-checksum)" >> $GITHUB_OUTPUT - - name: "cp PHAR" - run: cp tmp/phpstan.phar phpstan-dist/phpstan.phar + - name: "Checkout phpstan-src" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: phpstan-src + + - name: "Get Git log" + id: git-log + working-directory: phpstan-src + run: | + echo "log<> $GITHUB_OUTPUT + echo "$(git log ${{ steps.previous-commit.outputs.sha }}..${{ github.event.after }} --reverse --pretty='https://github.com/phpstan/phpstan-src/commit/%H %s')" >> $GITHUB_OUTPUT + echo 'MESSAGE' >> $GITHUB_OUTPUT + + - name: "Get short phpstan-src SHA" + id: short-src-sha + working-directory: phpstan-src + run: echo "sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT + + - name: "Check PHAR checksum" + id: checksum-difference + working-directory: phpstan-dist + run: | + checksum=${{needs.compiler-tests.outputs.checksum}} + if [[ $(head -n 1 .phar-checksum) != "$checksum" ]]; then + echo "result=different" >> $GITHUB_OUTPUT + else + echo "result=same" >> $GITHUB_OUTPUT + fi + + - name: "Download phpstan.phar" + uses: actions/download-artifact@v4 + with: + name: phar-file + + - name: "mv PHAR" + run: mv phpstan.phar phpstan-dist/phpstan.phar + + - name: "chmod PHAR" + run: chmod 755 phpstan-dist/phpstan.phar + + - name: "Update checksum" + run: | + echo ${{needs.compiler-tests.outputs.checksum}} > phpstan-dist/.phar-checksum + echo ${{ github.event.head_commit.id }} >> phpstan-dist/.phar-checksum - name: "Sign PHAR" working-directory: phpstan-dist run: rm phpstan.phar.asc && gpg --command-fd 0 --pinentry-mode loopback -u "$GPG_ID" --batch --detach-sign --armor --output phpstan.phar.asc phpstan.phar env: - GPG_ID: ${{ secrets.GPG_ID }} + GPG_ID: ${{ steps.import-gpg.outputs.fingerprint }} - name: "Verify PHAR" working-directory: phpstan-dist run: "gpg --verify phpstan.phar.asc" - - name: "Set Git signing key" - working-directory: phpstan-dist - run: git config user.signingkey "$GPG_ID" - env: - GPG_ID: ${{ secrets.GPG_ID }} - - - name: "Configure Git" - working-directory: phpstan-dist - run: | - git config user.email "ondrej@mirtes.cz" && \ - git config user.name "Ondrej Mirtes" + - name: "Install lucky_commit" + uses: baptiste0928/cargo-install@v3 + with: + crate: lucky_commit + args: --no-default-features - - name: "Commit PHAR - master" + - name: "Commit PHAR - development" + if: "!startsWith(github.ref, 'refs/tags/') && steps.checksum-difference.outputs.result == 'different'" working-directory: phpstan-dist - if: "!startsWith(github.ref, 'refs/tags/')" + env: + INPUT_LOG: ${{ steps.git-log.outputs.log }} run: | - git add phpstan.phar phpstan.phar.asc && \ - git commit -S -m "Updated PHPStan to commit ${{ github.event.after }}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master + git config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git add . + git commit --gpg-sign -m "Updated PHPStan to commit ${{ github.event.after }}" -m "$INPUT_LOG" --author "phpstan-bot " + lucky_commit ${{ steps.short-src-sha.outputs.sha }} + git push - name: "Commit PHAR - tag" - working-directory: phpstan-dist if: "startsWith(github.ref, 'refs/tags/')" - run: | - git add phpstan.phar phpstan.phar.asc && \ - git commit -S -m "PHPStan ${GITHUB_REF#refs/tags/}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master && \ - git tag -s ${GITHUB_REF#refs/tags/} -m "${GITHUB_REF#refs/tags/}" && \ - git push --quiet origin ${GITHUB_REF#refs/tags/} + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_user_name: "phpstan-bot" + commit_user_email: "ondrej+phpstanbot@mirtes.cz" + commit_author: "phpstan-bot " + commit_options: "--gpg-sign" + repository: phpstan-dist + commit_message: "PHPStan ${{github.ref_name}}" + tagging_message: ${{github.ref_name}} diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml new file mode 100644 index 0000000000..34ef71bb83 --- /dev/null +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -0,0 +1,24 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Base PR on previous branch" + +on: + pull_request_target: + types: + - opened + branches: + - '2.2.x' + + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml new file mode 100644 index 0000000000..b9785a2a3c --- /dev/null +++ b/.github/workflows/pr-marked-as-ready.yml @@ -0,0 +1,21 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Pull request ready for review" + +on: + pull_request_target: + types: + - ready_for_review + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "This pull request has been marked as ready for review." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml new file mode 100644 index 0000000000..7f962ce2c0 --- /dev/null +++ b/.github/workflows/reflection-golden-test.yml @@ -0,0 +1,112 @@ +# 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 + + - uses: "ramsey/composer-install@v3" + + - 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: + - "8.2" + - "8.3" + - "8.4" + - "8.5" + + 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 + + - uses: "ramsey/composer-install@v3" + + - 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 + + - uses: "ramsey/composer-install@v3" + + - name: "Reflection golden test" + run: "make tests-golden-reflection || true" diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 0000000000..b2f810732c --- /dev/null +++ b/.github/workflows/spelling.yml @@ -0,0 +1,23 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Spelling" + +on: + pull_request: + push: + branches: + - "2.1.x" + +jobs: + typos: + name: "Check for typos" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Check for typos" + uses: "crate-ci/typos@v1" + with: + files: "README.md src/" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index be5b2663c8..ec4fb7ba05 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -4,89 +4,123 @@ name: "Static Analysis" on: pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' push: branches: - - "master" + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" +concurrency: + group: sa-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: static-analysis: name: "PHPStan" runs-on: ${{ matrix.operating-system }} - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" operating-system: [ubuntu-latest, windows-latest] - script: - - "make phpstan" - - "make phpstan-static-reflection" - - "make phpstan-validate-stub-files" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + shell: bash + run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 --update-with-dependencies --ignore-platform-reqs" + + - uses: "ramsey/composer-install@v3" + + - name: "Change to simple-downgrade PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.4" + ini-file: development + extensions: mbstring - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + shell: bash + run: | + composer install --no-interaction --no-progress --working-dir=compiler + ./compiler/vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }} + composer dump + + - name: "Re-store PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + ini-file: development + extensions: mbstring - name: "PHPStan" - run: ${{ matrix.script }} + run: "make phpstan" static-analysis-with-result-cache: name: "PHPStan with result cache" runs-on: "ubuntu-latest" - timeout-minutes: 30 + timeout-minutes: 60 strategy: + fail-fast: false matrix: php-version: - - "7.4" + - "8.2" + - "8.3" + - "8.4" + - "8.5" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - name: "Cache Result cache" - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ./tmp - key: "result-cache-v4" + key: "result-cache-v14-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v14-${{ matrix.php-version }}- - name: "PHPStan with result cache" run: | @@ -97,38 +131,52 @@ jobs: make phpstan-result-cache make phpstan-result-cache - - name: "Upload result cache artifact" - uses: actions/upload-artifact@v2 - with: - name: resultCache-ubuntu-latest.php - path: tmp/resultCache.php - generate-baseline: name: "Generate baseline" runs-on: "ubuntu-latest" - timeout-minutes: 30 - - strategy: - matrix: - php-version: - - "7.4" + timeout-minutes: 60 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.2" + ini-file: development - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - 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.2" + ini-file: development + + - uses: "ramsey/composer-install@v3" + + - 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..d5dbc90b82 --- /dev/null +++ b/.github/workflows/tests-levels-matrix.php @@ -0,0 +1,38 @@ +tests as $testClasses) { + foreach($testClasses->testClass as $testClass) { + foreach($testClass->testMethod as $testMethod) { + $testCaseName = (string)$testMethod['id']; + + [$className, $testName] = explode('::', $testCaseName, 2); + $fileName = 'tests/' . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + + $filter = str_replace('\\', '\\\\', $testCaseName); + + $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + } + } +} + +if ($testFilters === []) { + throw new RuntimeException('No tests found'); +} + +$chunkSize = (int) ceil(count($testFilters) / 10); +$chunks = array_chunk($testFilters, $chunkSize); + +$commands = []; +foreach ($chunks as $chunk) { + $commands[] = implode("\n", array_map(fn (string $ch) => sprintf('php vendor/bin/phpunit %s --group levels', $ch), $chunk)); +} + +echo json_encode($commands); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2a1ecd903..cff6d8792a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,35 +4,43 @@ name: "Tests" on: pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "master" + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.0.x-dev" +concurrency: + group: tests-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: tests: name: "Tests" runs-on: ${{ matrix.operating-system }} - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: php-version: - - "7.3" - - "7.4" - - "8.0" + - "8.2" + - "8.3" + - "8.4" + - "8.5" operating-system: [ ubuntu-latest, windows-latest ] - script: - - "make tests" - - "make tests-static-reflection" - - "make tests-integration" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -41,88 +49,167 @@ jobs: php-version: "${{ matrix.php-version }}" tools: pecl extensions: ds,mbstring - ini-values: memory_limit=640M + ini-file: development + ini-values: memory_limit=2G - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + - uses: "ramsey/composer-install@v3" - name: "Tests" - run: "${{ matrix.script }}" + run: "make tests" - tests-old-phpunit: - name: "Tests with old PHPUnit" + tests-integration: + name: "Integration tests" runs-on: ${{ matrix.operating-system }} - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: - php-version: - - "7.1" - - "7.2" operating-system: [ ubuntu-latest, windows-latest ] - script: - - "make tests-coverage" - - "make tests-static-reflection-coverage" - - "make tests-integration-coverage" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.2" tools: pecl extensions: ds,mbstring - ini-values: memory_limit=640M + ini-file: development + ini-values: memory_limit=1G - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - uses: "ramsey/composer-install@v3" - - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies" + - name: "Tests" + run: "make tests-integration" - - name: "Transform source code" - run: php bin/transform-source.php + 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 + + - uses: "ramsey/composer-install@v3" + + - 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 + + - uses: "ramsey/composer-install@v3" - name: "Tests" run: "${{ matrix.script }}" - tests-code-coverage: - name: "Tests with code coverage" + tests-with-old-phpunit: + name: "Tests with old PHPUnit" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 - runs-on: "ubuntu-latest" - timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + operating-system: [ ubuntu-latest, windows-latest ] steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: - coverage: "pcov" - php-version: "7.4" + coverage: "none" + php-version: "${{ matrix.php-version }}" tools: pecl - extensions: ds + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=2G - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Downgrade PHPUnit" + shell: bash + run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 --update-with-dependencies --ignore-platform-reqs" - - name: "Tests" - run: | - php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude="~vendor~" vendor/bin/phpunit + - uses: "ramsey/composer-install@v3" + + - name: "Downgrade PHPUnit with Paratest" + shell: bash + run: "composer require --dev phpunit/phpunit:^9.6 brianium/paratest:^6.5 symfony/console:^5.4 symfony/process:^5.4 --update-with-dependencies --ignore-platform-reqs --working-dir=tests" - - name: "Coveralls" - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Change to simple-downgrade PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.4" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=2G + + - name: "Transform source code" + shell: bash run: | - composer require twinh/php-coveralls --dev && \ - vendor/bin/php-coveralls --verbose --coverage_clover=tests/tmp/clover.xml --json_path=tests/tmp/coveralls-upload.json + composer install --no-interaction --no-progress --working-dir=compiler + ./compiler/vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }} + composer dump + + - name: "Re-store PHP version" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=2G + + - name: "Tests" + run: "make tests" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml new file mode 100644 index 0000000000..ad4ae46d60 --- /dev/null +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -0,0 +1,49 @@ +# 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.2" + - uses: "ramsey/composer-install@v3" + - 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 625cace6ae..c4d0656bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,12 @@ /compiler/tmp /compiler/vendor /conf/config.local.yml +/build-cs /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/.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 index 9d85ecb989..3cb07512ee 100644 --- a/Makefile +++ b/Makefile @@ -2,36 +2,32 @@ build: cs tests phpstan -tests: - php vendor/bin/paratest --no-coverage +tests: install-paratest + XDEBUG_MODE=off php tests/vendor/bin/paratest --runner WrapperRunner --no-coverage -tests-integration: - php vendor/bin/paratest --no-coverage --group exec +tests-integration: install-paratest + php tests/vendor/bin/paratest --runner WrapperRunner --no-coverage --group exec -tests-static-reflection: - php vendor/bin/paratest --no-coverage --bootstrap tests/bootstrap-static-reflection.php - -tests-coverage: - php vendor/bin/paratest - -tests-integration-coverage: - php vendor/bin/paratest --group exec - -tests-static-reflection-coverage: - php vendor/bin/paratest --bootstrap tests/bootstrap-static-reflection.php +tests-golden-reflection: + php vendor/bin/phpunit tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php lint: - php vendor/bin/parallel-lint --colors \ + XDEBUG_MODE=off php vendor/bin/parallel-lint --colors \ --exclude tests/PHPStan/Analyser/data \ + --exclude tests/PHPStan/Analyser/nsrt \ --exclude tests/PHPStan/Rules/Methods/data \ --exclude tests/PHPStan/Rules/Functions/data \ + --exclude tests/PHPStan/Rules/Names/data \ --exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \ --exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \ --exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \ + --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 \ @@ -41,31 +37,124 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \ --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ - src tests compiler/src - -cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs - -cs-fix: - php build-cs/vendor/bin/phpcbf + --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 \ + --exclude tests/PHPStan/Rules/Playground/data/promote-missing-override.php \ + --exclude tests/PHPStan/Rules/Traits/data/trait-attributes.php \ + --exclude tests/PHPStan/Rules/Classes/data/non-class-attribute-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php \ + --exclude tests/PHPStan/Rules/Classes/data/class-attributes.php \ + --exclude tests/PHPStan/Rules/Classes/data/enum-attributes.php \ + --exclude tests/PHPStan/Rules/Cast/data/void-cast.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php \ + src tests + +install-paratest: + composer install --working-dir tests + +.PHONY: cs-install +cs-install: + git clone https://github.com/phpstan/build-cs.git || true + git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x + composer install --working-dir build-cs + +.PHONY: cs +cs: cs-install + XDEBUG_MODE=off php build-cs/vendor/bin/phpcs + +.PHONY: cs-fix +cs-fix: cs-install + XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf phpstan: - php bin/phpstan clear-result-cache -q && php -d memory_limit=768M bin/phpstan - -phpstan-static-reflection: - php bin/phpstan clear-result-cache -q && php -d memory_limit=800M bin/phpstan analyse -c phpstan-static-reflection.neon + php bin/phpstan clear-result-cache -q && php -d memory_limit=448M bin/phpstan phpstan-result-cache: - php -d memory_limit=768M bin/phpstan + php -d memory_limit=448M bin/phpstan + +phpstan-fix: + php -d memory_limit=448M bin/phpstan --fix phpstan-generate-baseline: - php -d memory_limit=768M bin/phpstan --generate-baseline + php -d memory_limit=448M bin/phpstan --generate-baseline -phpstan-validate-stub-files: - php bin/phpstan analyse -c conf/config.stubFiles.neon -l 8 tests/notAutoloaded/empty.php +phpstan-generate-baseline-php: + php -d memory_limit=448M bin/phpstan analyse --generate-baseline phpstan-baseline.php phpstan-pro: - php -d memory_limit=768M bin/phpstan --pro + php -d memory_limit=448M bin/phpstan --pro + +name-collision: + php vendor/bin/detect-collisions --configuration build/collision-detector.json -composer-require-checker: - php build/composer-require-checker.phar check --config-file $(CURDIR)/build/composer-require-checker.json +composer-dependency-analyser: + php vendor/bin/composer-dependency-analyser --config build/composer-dependency-analyser.php diff --git a/README.md b/README.md index 6692591ca7..e817604542 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PHPStan - PHP Static Analysis Tool -[![Build](https://github.com/phpstan/phpstan-src/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-src/actions) -[![Coverage Status](https://coveralls.io/repos/github/phpstan/phpstan-src/badge.svg)](https://coveralls.io/github/phpstan/phpstan-src) +[![Build](https://github.com/phpstan/phpstan-src/workflows/Tests/badge.svg)](https://github.com/phpstan/phpstan-src/actions) [![PHPStan Enabled](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) --- @@ -12,11 +11,19 @@ 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 @@ -40,8 +47,6 @@ To detect code style issues, run: make cs ``` -This requires PHP 7.4. On older versions the build target will be skipped and succeed silently. - And then to fix code style, run: ```bash @@ -57,7 +62,7 @@ make tests ### Debugging -1. Make sure XDebug is installed and configured. +1. Make sure Xdebug is installed and configured. 2. Add `--xdebug` option when running PHPStan. Without it PHPStan turns the debugger off at runtime. 3. If you're not debugging the [result cache](https://phpstan.org/user-guide/result-cache), also add the `--debug` option. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000000..1b76768ec4 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,338 @@ +Upgrading from PHPStan 1.x to 2.0 +================================= + +## PHP version requirements + +PHPStan now requires PHP 7.4 or newer to run. + +## Upgrading guide for end users + +The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release** +and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users. + +Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules). + +Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`: + +```json +"require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + ... +} +``` + +Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well. + +After changing your `composer.json`, run `composer update 'phpstan/*' -W`. + +It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0. + +### Noteworthy changes to code analysis + +* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) +* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type) +* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) +* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported. + +### Removed option `checkMissingIterableValueType` + +It's strongly recommended to add the missing array typehints. + +If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.iterableValue +``` + +### Removed option `checkGenericClassInNonGenericObjectType` + +It's strongly recommended to add the missing generic typehints. + +If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.generics +``` + +### Removed `checkAlwaysTrue*` options + +These options have been removed because PHPStan now always behaves as if these were set to `true`: + +* `checkAlwaysTrueCheckTypeFunctionCall` +* `checkAlwaysTrueInstanceof` +* `checkAlwaysTrueStrictComparison` +* `checkAlwaysTrueLooseComparison` + +### Removed option `excludes_analyse` + +It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files). + +### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern + +If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`: + +```neon +parameters: + excludePaths: + - tests/*/data/* + - src/broken + - node_modules (?) # optional path, might not exist +``` + +If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`. + +```neon +parameters: + reportUnmatchedIgnoredErrors: false +``` + +Appending `(?)` in `ignoreErrors` is not supported. + +### Changes in 1st party PHPStan extensions + +* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) + * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) + * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) +* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) + * Removed legacy options with `_` in the name + * `container_xml_path` -> use `containerXmlPath` + * `constant_hassers` -> use `constantHassers` + * `console_application_loader` -> use `consoleApplicationLoader` + +### Minor backward compatibility breaks + +* Removed unused config parameter `cache.nodesByFileCountMax` +* Removed unused config parameter `memoryLimitFile` +* Removed unused feature toggle `disableRuntimeReflectionProvider` +* Removed unused config parameter `staticReflectionClassNamePatterns` +* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead +* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead +* `additionalConfigFiles` config parameter must be a list + +## Upgrading guide for extension developers + +> [!NOTE] +> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code. +> +> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version. + +### PHPStan now uses nikic/php-parser v5 + +See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser. + +The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class. + +Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The +`Stmt\Throw_` class has been removed. + +### PHPStan now uses phpstan/phpdoc-parser v2 + +See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser. + +### Returning plain strings as errors no longer supported, use RuleErrorBuilder + +Identifiers are also required in custom rules. + +Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) + +**Before**: + +```php +return ['My error']; +``` + +**After**: + +```php +return [ + RuleErrorBuilder::message('My error') + ->identifier('my.error') + ->build(), +]; +``` + +### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface + +Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) + +### Removed deprecated `ParametersAcceptorSelector::selectSingle()` + +Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions. + +**Before**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); +``` + +**After**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() +)->getReturnType(); +``` + +If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object: + +* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html) +* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html) +* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html) +* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html) +* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction) + +**Before**: + +```php +$function = $node->getFunctionReflection(); +$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); +``` + +**After**: + +```php +$returnType = $node->getFunctionReflection()->getReturnType(); +``` + +### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters + +[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required): + +* `Expr $expr` +* `Type $type` +* `TypeSpecifierContext $context` +* `Scope $scope` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable). + +[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts: + +* `array $sureTypes` +* `array $sureNotTypes` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable). + +### `ConstantArrayType` no longer extends `ArrayType` + +`Type::getArrays()` now returns `list`. + +Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal. + +### Changed `TypeSpecifier::specifyTypesInCondition()` + +This method now longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable). + +### Node attributes `parent`, `previous`, `next` are no longer available + +Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules + +### Removed config parameter `scopeClass` + +As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service. + +### Removed `PHPStan\Broker\Broker` + +Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead. + +`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead. + +Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`. + +### List type is enabled for everyone + +Removed static methods from `AccessoryArrayListType` class: + +* `isListTypeEnabled()` +* `setListTypeEnabled()` +* `intersectWith()` + +Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`. + +### Minor backward compatibility breaks + +* Classes that were previously `@final` were made `final` +* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required +* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required +* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties) +* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes) +* Remove `ArrayType::generalizeKeys()` +* Remove `ArrayType::count()`, use `Type::getArraySize()` instead +* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead +* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead +* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead +* Remove unused `PHPStanTestCase::$useStaticReflectionProvider` +* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead +* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead +* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead +* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()` +* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead +* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead +* Rename `Type::isClassStringType()` to `Type::isClassString()` +* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead +* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead +* Remove `ConstantArrayType::getNextAutoIndex()` +* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type` + * Use `getFirstIterable*Type` and `getLastIterable*Type` instead +* Remove `ConstantArrayType::generalizeToArray()` +* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead +* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead +* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead +* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead +* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead +* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead +* Made `TypeUtils` thinner by removing methods: + * Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead + * Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead + * Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead + * Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) + * Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead + * Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead + * Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead + * Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead + * Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead +* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead +* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool` +* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list`, no longer `int` +* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead +* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool` +* Remove `FunctionReflection::isFinal()` +* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html) +* Remove `__set_state()` on objects that should not be serialized in cache +* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string` +* `LevelsTestCase::dataTopics()` data provider made static +* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint +* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Changes around `ClassConstantReflection` + * Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection` + * Interface `ConstantReflection` renamed to `ClassConstantReflection` + * Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection` + * Interface `GlobalConstantReflection` renamed to `ConstantReflection` +* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*` + * `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor` + * `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection` + * `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant` +* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` +* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node + * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node diff --git a/build-cs/.gitignore b/apigen/.gitignore similarity index 100% rename from build-cs/.gitignore rename to apigen/.gitignore 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..827002329a --- /dev/null +++ b/apigen/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "php": "^8.1" + }, + "require-dev": { + "apigen/apigen": "dev-master#aa151a961053d20e46d2c7da65cbb03c130d12ff" + }, + "autoload": { + "psr-4": { + "PHPStan\\ApiGen\\": "src" + } + } +} diff --git a/apigen/composer.lock b/apigen/composer.lock new file mode 100644 index 0000000000..36e6073f5e --- /dev/null +++ b/apigen/composer.lock @@ -0,0 +1,1713 @@ +{ + "_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": "f25de5a945d9862c3ba649cab28f3e61", + "packages": [], + "packages-dev": [ + { + "name": "apigen/apigen", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ApiGen/ApiGen.git", + "reference": "aa151a961053d20e46d2c7da65cbb03c130d12ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ApiGen/ApiGen/zipball/aa151a961053d20e46d2c7da65cbb03c130d12ff", + "reference": "aa151a961053d20e46d2c7da65cbb03c130d12ff", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "jetbrains/phpstorm-stubs": "^2024.2", + "latte/latte": "^3.0", + "league/commonmark": "^2.3", + "nette/di": "^3.1", + "nette/finder": "^3.0", + "nette/schema": "^1.2", + "nette/utils": "^4.0", + "nikic/php-parser": "^5.3", + "php": "^8.1", + "phpstan/php-8-stubs": "^0.4.0", + "phpstan/phpdoc-parser": "^1.16", + "symfony/console": "^6.4" + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-mbstring": "*", + "symfony/polyfill-php80": "*" + }, + "require-dev": { + "nette/neon": "^3.4", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.9", + "tracy/tracy": "^2.9" + }, + "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": "2025-02-21T14:27:46+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": "jetbrains/phpstorm-stubs", + "version": "v2024.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "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" + }, + "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/v2024.3" + }, + "time": "2024-12-14T08:03:12+00:00" + }, + { + "name": "latte/latte", + "version": "v3.0.23", + "source": { + "type": "git", + "url": "https://github.com/nette/latte.git", + "reference": "3198a4e336a2a1e535924af11d9a63fbf1650836" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/latte/zipball/3198a4e336a2a1e535924af11d9a63fbf1650836", + "reference": "3198a4e336a2a1e535924af11d9a63fbf1650836", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/application": "<3.1.7", + "nette/caching": "<3.1.4" + }, + "require-dev": { + "nette/php-generator": "^4.0", + "nette/tester": "^2.5", + "nette/utils": "^4.0", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.10" + }, + "suggest": { + "ext-fileinfo": "to use filter |datastream", + "ext-iconv": "to use filters |reverse, |substring", + "ext-intl": "to use Latte\\Engine::setLocale()", + "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": { + "psr-4": { + "Latte\\": "src/Latte" + }, + "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.23" + }, + "time": "2025-07-17T01:01:46+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "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/process": "^5.4 | ^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.8-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": "2025-05-05T12:20:28+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/di", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "57f923a7af32435b6e4921c0adbc70c619625a17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/57f923a7af32435b6e4921c0adbc70c619625a17", + "reference": "57f923a7af32435b6e4921c0adbc70c619625a17", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^4.1.6", + "nette/robot-loader": "^4.0", + "nette/schema": "^1.2.5", + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "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 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.2.4" + }, + "time": "2025-01-10T04:57:37+00:00" + }, + { + "name": "nette/finder", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "027395c638637de95c8e9fad49a7c51249404ed2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/027395c638637de95c8e9fad49a7c51249404ed2", + "reference": "027395c638637de95c8e9fad49a7c51249404ed2", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "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/v3.0.0" + }, + "time": "2022-12-14T17:05:54+00:00" + }, + { + "name": "nette/neon", + "version": "v3.4.4", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/3411aa86b104e2d5b7e760da4600865ead963c3c", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "8.0 - 8.4" + }, + "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.4" + }, + "time": "2024-10-04T22:00:08+00:00" + }, + { + "name": "nette/php-generator", + "version": "v4.1.8", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.9 || ^4.0", + "php": "8.0 - 8.4" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.18 || ^5.0", + "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.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 PHP Generator: generates neat PHP code for you. Supports new PHP 8.4 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.1.8" + }, + "time": "2025-03-31T00:29:29+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v4.0.3", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/45d67753fb4865bb718e9a6c9be69cc9470137b7", + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/utils": "^4.0", + "php": "8.0 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "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 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/v4.0.3" + }, + "time": "2024-06-18T20:26:39+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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 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.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "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()" + }, + "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 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/v4.0.7" + }, + "time": "2025-06-03T04:55:08+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "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.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phpstan/php-8-stubs", + "version": "0.4.14", + "source": { + "type": "git", + "url": "https://github.com/phpstan/php-8-stubs.git", + "reference": "66b0ffe2d4dba18f12ffee663cdbe31640819e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/66b0ffe2d4dba18f12ffee663cdbe31640819e7a", + "reference": "66b0ffe2d4dba18f12ffee663cdbe31640819e7a", + "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.14" + }, + "time": "2025-06-16T07:31:03+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.33.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "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.33.0" + }, + "time": "2024-10-13T11:25:22+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.4.23", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", + "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.23" + }, + "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": "2025-06-27T19:37:22+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.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.32.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.32.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.32.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.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "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": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "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.6.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": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "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.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": "2025-04-20T20:19:01+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.6.0" +} diff --git a/apigen/src/Filter.php b/apigen/src/Filter.php new file mode 100644 index 0000000000..66f39c3bfb --- /dev/null +++ b/apigen/src/Filter.php @@ -0,0 +1,170 @@ +namespacedName->toString(); + if (Strings::startsWith($name, 'PhpParser\\')) { + return true; + } + + if (!Strings::startsWith($name, 'PHPStan\\')) { + return false; + } + + if (Strings::startsWith($name, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($name, 'PHPStan\\BetterReflection\\')) { + return true; + } + + if ($this->hasApiTag($node)) { + return true; + } + + foreach ($node->getMethods() as $method) { + if ($this->hasApiTag($method)) { + return true; + } + } + + return false; + } + + public function filterClassLikeTags(array $tags): bool + { + return parent::filterClassLikeTags($tags); + } + + public function filterClassLikeInfo(ClassLikeInfo $info): bool + { + return parent::filterClassLikeInfo($info); + } + + public function filterFunctionNode(Node\Stmt\Function_ $node): bool + { + $name = $node->namespacedName->toString(); + if (!Strings::startsWith($name, 'PHPStan\\')) { + return false; + } + + return $this->hasApiTag($node); + } + + public function filterFunctionTags(array $tags): bool + { + return parent::filterFunctionTags($tags); + } + + public function filterFunctionInfo(FunctionInfo $info): bool + { + return parent::filterFunctionInfo($info); + } + + public function filterConstantNode(Node\Stmt\ClassConst $node): bool + { + return parent::filterConstantNode($node); + } + + public function filterPropertyNode(Node\Stmt\Property $node): bool + { + return parent::filterPropertyNode($node); + } + + public function filterPromotedPropertyNode(Node\Param $node): bool + { + return parent::filterPromotedPropertyNode($node); + } + + public function filterMethodNode(Node\Stmt\ClassMethod $node): bool + { + return parent::filterMethodNode($node); + } + + public function filterEnumCaseNode(Node\Stmt\EnumCase $node): bool + { + return parent::filterEnumCaseNode($node); + } + + public function filterMemberTags(array $tags): bool + { + return parent::filterMemberTags($tags); + } + + public function filterMemberInfo(ClassLikeInfo $classLike, MemberInfo $member): bool + { + $className = $classLike->name->full; + if (Strings::startsWith($className, 'PhpParser\\')) { + return true; + } + if (Strings::startsWith($className, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($className, 'PHPStan\\BetterReflection\\')) { + return true; + } + if (!$member instanceof MethodInfo) { + return !Strings::startsWith($className, 'PHPStan\\'); + } + + if (!Strings::startsWith($className, 'PHPStan\\')) { + return false; + } + + if (isset($classLike->tags['api'])) { + return true; + } + + return isset($member->tags['api']); + } + + private function hasApiTag(Node $node): bool + { + $classDoc = $this->extractPhpDoc($node); + $tags = $this->extractTags($classDoc); + + return isset($tags['api']); + } + + private function extractPhpDoc(Node $node): PhpDocNode + { + return $node->getAttribute('phpDoc') ?? new PhpDocNode([]); + } + + /** + * @return PhpDocTagValueNode[][] indexed by [tagName][] + */ + private function extractTags(PhpDocNode $node): array + { + $tags = []; + + foreach ($node->getTags() as $tag) { + if ($tag->value instanceof InvalidTagValueNode) { + continue; + } + + $tags[substr($tag->name, 1)][] = $tag->value; + } + + return $tags; + } + +} diff --git a/apigen/src/RendererFilter.php b/apigen/src/RendererFilter.php new file mode 100644 index 0000000000..9f25bbd5d8 --- /dev/null +++ b/apigen/src/RendererFilter.php @@ -0,0 +1,113 @@ +children as $child) { + if ($this->filterNamespacePage($child)) { + return true; + } + } + + foreach ($namespace->class as $class) { + if ($this->filterClassLikePage($class)) { + return true; + } + } + + foreach ($namespace->interface as $interface) { + if ($this->filterClassLikePage($interface)) { + return true; + } + } + + foreach ($namespace->trait as $trait) { + if ($this->filterClassLikePage($trait)) { + return true; + } + } + + foreach ($namespace->enum as $enum) { + if ($this->filterClassLikePage($enum)) { + return true; + } + } + + foreach ($namespace->exception as $exception) { + if ($this->filterClassLikePage($exception)) { + return true; + } + } + + foreach ($namespace->function as $function) { + if ($this->filterFunctionPage($function)) { + return true; + } + } + + return false; + } + + public function filterClassLikePage(ClassLikeInfo $classLike): bool + { + return $this->isClassRendered($classLike); + } + + private function isClassRendered(ClassLikeInfo $classLike): bool + { + $className = $classLike->name->full; + if (Strings::startsWith($className, 'PhpParser\\')) { + return true; + } + if (Strings::startsWith($className, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($className, 'PHPStan\\BetterReflection\\')) { + return true; + } + + if (!Strings::startsWith($className, 'PHPStan\\')) { + return false; + } + + if (isset($classLike->tags['api'])) { + return true; + } + + foreach ($classLike->methods as $method) { + if (isset($method->tags['api'])) { + return true; + } + } + + return false; + } + + public function filterFunctionPage(FunctionInfo $function): bool + { + return parent::filterFunctionPage($function); // todo + } + + public function filterSourcePage(FileIndex $file): bool + { + return parent::filterSourcePage($file); + } + +} diff --git a/apigen/theme/blocks/head.latte b/apigen/theme/blocks/head.latte new file mode 100644 index 0000000000..16f25417e3 --- /dev/null +++ b/apigen/theme/blocks/head.latte @@ -0,0 +1,8 @@ +{define head} + + +{/define} +{define menu} + + {include #parent} +{/define} diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index b7505a6f6f..ed5d223fa8 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -31,11 +31,14 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -45,6 +48,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -61,8 +65,63 @@ '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], @@ -71,6 +130,12 @@ 'random_int' => ['hasSideEffects' => true], // methods + 'DateTimeInterface::diff' => ['hasSideEffects' => false], + 'DateTimeInterface::format' => ['hasSideEffects' => false], + 'DateTimeInterface::getOffset' => ['hasSideEffects' => false], + 'DateTimeInterface::getTimestamp' => ['hasSideEffects' => false], + 'DateTimeInterface::getTimezone' => ['hasSideEffects' => false], + 'DateTime::createFromFormat' => ['hasSideEffects' => false], 'DateTime::createFromImmutable' => ['hasSideEffects' => false], 'DateTime::getLastErrors' => ['hasSideEffects' => false], @@ -104,4 +169,45 @@ 'DateTimeImmutable::getOffset' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], + + // affects isConnected() + 'Redis::connect' => ['hasSideEffects' => true], + 'Redis::pconnect' => ['hasSideEffects' => true], + + 'SplDoublyLinkedList::pop' => ['hasSideEffects' => true], + 'SplDoublyLinkedList::shift' => ['hasSideEffects' => true], + + '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], + + 'SplFixedArray::extract' => ['hasSideEffects' => true], + + 'SplHead::extract' => ['hasSideEffects' => true], + 'SplHead::insert' => ['hasSideEffects' => true], + 'SplHead::recoverFromCorruption' => ['hasSideEffects' => true], + + 'SplObjectStorage::addAll' => ['hasSideEffects' => true], + 'SplObjectStorage::attach' => ['hasSideEffects' => true], + 'SplObjectStorage::detach' => ['hasSideEffects' => true], + 'SplObjectStorage::removeAll' => ['hasSideEffects' => true], + 'SplObjectStorage::removeAllExcept' => ['hasSideEffects' => true], + + 'SplPriorityQueue::extract' => ['hasSideEffects' => true], + 'SplPriorityQueue::insert' => ['hasSideEffects' => true], + 'SplPriorityQueue::recoverFromCorruption' => ['hasSideEffects' => true], + + 'SplQueue::dequeue' => ['hasSideEffects' => true], + + 'XmlReader::next' => ['hasSideEffects' => true], + 'XmlReader::read' => ['hasSideEffects' => true], ]; diff --git a/bin/generate-changelog.php b/bin/generate-changelog.php deleted file mode 100755 index 5119d62564..0000000000 --- a/bin/generate-changelog.php +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env php -setName('run'); - $this->addArgument('fromCommit', InputArgument::REQUIRED); - $this->addArgument('toCommit', InputArgument::REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $commitLines = $this->exec(['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit')), '--reverse', '--pretty=%H %s']); - $commits = array_map(function (string $line): array { - [$hash, $message] = explode(' ', $line, 2); - - return [ - 'hash' => $hash, - 'message' => $message - ]; - }, explode("\n", $commitLines)); - - $i = 0; - - foreach ($commits as $commit) { - $searchPullRequestsResponse = Request::get(sprintf('https://api.github.com/search/issues?q=repo:phpstan/phpstan-src+%s', $commit['hash'])) - ->sendsAndExpectsType('application/json') - ->basicAuth('ondrejmirtes', getenv('GITHUB_TOKEN')) - ->send(); - if ($searchPullRequestsResponse->code !== 200) { - $output->writeln(var_export($searchPullRequestsResponse->body, true)); - throw new \InvalidArgumentException((string) $searchPullRequestsResponse->code); - } - $searchPullRequestsResponse = $searchPullRequestsResponse->body; - - $searchIssuesResponse = Request::get(sprintf('https://api.github.com/search/issues?q=repo:phpstan/phpstan+%s', $commit['hash'])) - ->sendsAndExpectsType('application/json') - ->basicAuth('ondrejmirtes', getenv('GITHUB_TOKEN')) - ->send(); - if ($searchIssuesResponse->code !== 200) { - $output->writeln(var_export($searchIssuesResponse->body, true)); - throw new \InvalidArgumentException((string) $searchIssuesResponse->code); - } - $searchIssuesResponse = $searchIssuesResponse->body; - $items = array_merge($searchPullRequestsResponse->items, $searchIssuesResponse->items); - $parenthesis = 'https://github.com/phpstan/phpstan-src/commit/' . $commit['hash']; - $thanks = null; - $issuesToReference = []; - foreach ($items as $responseItem) { - if (isset($responseItem->pull_request)) { - $parenthesis = sprintf('[#%d](%s)', $responseItem->number, 'https://github.com/phpstan/phpstan-src/pull/' . $responseItem->number); - $thanks = $responseItem->user->login; - } else { - $issuesToReference[] = sprintf('#%d', $responseItem->number); - } - } - - $output->writeln(sprintf('* %s (%s)%s%s', $commit['message'], $parenthesis, count($issuesToReference) > 0 ? ', ' . implode(', ', $issuesToReference) : '', $thanks !== null ? sprintf(', thanks @%s!', $thanks) : '')); - - if ($i > 0 && $i % 8 === 0) { - sleep(60); - } - - $i++; - } - - return 0; - } - - /** - * @param string[] $commandParts - * @return string - */ - private function exec(array $commandParts): string - { - $command = implode(' ', array_map(function (string $part): string { - return escapeshellarg($part); - }, $commandParts)); - - exec($command, $outputLines, $statusCode); - $output = implode("\n", $outputLines); - if ($statusCode !== 0) { - throw new \InvalidArgumentException(sprintf('Command %s failed: %s', $command, $output)); - } - - return $output; - } - - }; - - $application = new \Symfony\Component\Console\Application(); - $application->add($command); - $application->setDefaultCommand('run', true); - $application->run(); - -})(); diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index a7e4ebd35a..d161d374e4 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -1,36 +1,87 @@ #!/usr/bin/env php -create(ParserFactory::ONLY_PHP7); - $finder = new Symfony\Component\Finder\Finder(); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $finder = new Finder(); $finder->in(__DIR__ . '/../vendor/jetbrains/phpstorm-stubs')->files()->name('*.php'); - $visitor = new class() extends \PhpParser\NodeVisitorAbstract { + $visitor = new class() extends NodeVisitorAbstract { /** @var string[] */ - public $functions = []; + public array $functions = []; + + /** @var list */ + public array $impureFunctions = []; /** @var string[] */ - public $methods = []; + public array $methods = []; public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Function_) { + assert(isset($node->namespacedName)); + $functionName = $node->namespacedName->toLowerString(); + foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { - $this->functions[] = $node->namespacedName->toLowerString(); - break; + if ($attr->name->toString() !== Pure::class) { + continue; + } + + // The following functions have side effects, but their state is managed within the PHPStan scope: + if (in_array($functionName, [ + 'stat', + 'lstat', + 'file_exists', + 'is_writable', + 'is_writeable', + 'is_readable', + 'is_executable', + 'is_file', + 'is_dir', + 'is_link', + 'filectime', + 'fileatime', + 'filemtime', + 'fileinode', + 'filegroup', + 'fileowner', + 'filesize', + 'filetype', + 'fileperms', + 'ftell', + 'ini_get', + 'function_exists', + 'json_last_error', + 'json_last_error_msg', + ], true)) { + $this->functions[] = $functionName; + break 2; + } + + // PhpStorm stub's #[Pure(true)] means the function has side effects but its return value is important. + // In PHPStan's criteria, these functions are simply considered as ['hasSideEffect' => true]. + if (isset($attr->args[0]->value->name->name) && $attr->args[0]->value->name->name === 'true') { + $this->impureFunctions[] = $functionName; + } else { + $this->functions[] = $functionName; } + break 2; } } } @@ -38,14 +89,14 @@ public function enterNode(Node $node) if ($node instanceof Node\Stmt\ClassMethod) { $class = $node->getAttribute('parent'); if (!$class instanceof Node\Stmt\ClassLike) { - throw new \PHPStan\ShouldNotHappenException($node->name->toString()); + throw new ShouldNotHappenException($node->name->toString()); } $className = $class->namespacedName->toString(); foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { + if ($attr->name->toString() === Pure::class) { $this->methods[] = sprintf('%s::%s', $className, $node->name->toString()); - break; + break 2; } } } @@ -53,6 +104,7 @@ public function enterNode(Node $node) return null; } + }; foreach ($finder as $stubFile) { @@ -63,24 +115,46 @@ public function enterNode(Node $node) $traverser->addVisitor($visitor); $traverser->traverse( - $parser->parse(\PHPStan\File\FileReader::read($path)) + $parser->parse(FileReader::read($path)), ); } + /** @var array $metadata */ $metadata = require __DIR__ . '/functionMetadata_original.php'; foreach ($visitor->functions as $functionName) { if (array_key_exists($functionName, $metadata)) { if ($metadata[$functionName]['hasSideEffects']) { - throw new \PHPStan\ShouldNotHappenException($functionName); + throw new ShouldNotHappenException($functionName); } } $metadata[$functionName] = ['hasSideEffects' => false]; } + foreach ($visitor->impureFunctions as $functionName) { + if (in_array($functionName, [ + 'class_exists', + 'enum_exists', + 'interface_exists', + 'trait_exists', + ], true)) { + continue; + } + if (array_key_exists($functionName, $metadata)) { + if (in_array($functionName, [ + 'ob_get_contents', + ], true)) { + continue; + } + if (!$metadata[$functionName]['hasSideEffects']) { + throw new ShouldNotHappenException($functionName); + } + } + $metadata[$functionName] = ['hasSideEffects' => true]; + } foreach ($visitor->methods as $methodName) { if (array_key_exists($methodName, $metadata)) { if ($metadata[$methodName]['hasSideEffects']) { - throw new \PHPStan\ShouldNotHappenException($methodName); + throw new ShouldNotHappenException($methodName); } } $metadata[$methodName] = ['hasSideEffects' => false]; @@ -91,6 +165,20 @@ public function enterNode(Node $node) $template = <<<'php' true as a modification to bin/functionMetadata_original.php. + * 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs + * 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php. + */ + return [ %s ]; @@ -105,6 +193,5 @@ public function enterNode(Node $node) ); } - \PHPStan\File\FileWriter::write(__DIR__ . '/../resources/functionMetadata.php', sprintf($template, $content)); - + FileWriter::write(__DIR__ . '/../resources/functionMetadata.php', sprintf($template, $content)); })(); diff --git a/bin/generate-rule-error-classes.php b/bin/generate-rule-error-classes.php index 116910ee86..6b8284669e 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] === null ? '' : sprintf(': %s', $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 1a7a618c64..89bbe2c381 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -3,99 +3,126 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; +use PHPStan\Command\DiagnoseCommand; +use PHPStan\Command\DumpParametersCommand; use PHPStan\Command\FixerWorkerCommand; use PHPStan\Command\WorkerCommand; +use PHPStan\Internal\ComposerHelper; use Symfony\Component\Console\Helper\ProgressBar; (function () { - error_reporting(E_ALL); + error_reporting(E_ALL & ~E_DEPRECATED); ini_set('display_errors', 'stderr'); - if (version_compare(PHP_VERSION, '7.4.0', '<')) { - // PHP earlier than 7.4.x with OpCache triggers a bug when we intercept - // custom autoloaders' reads to discover file paths. See PHPStan #4881. - ini_set('opcache.enable', 'Off'); - } - gc_disable(); // performance boost define('__PHPSTAN_RUNNING__', true); + $analysisStartTime = microtime(true); + $devOrPharLoader = require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../preload.php'; - $devOrPharLoader->unregister(); + $composer = ComposerHelper::getComposerConfig(getcwd()); - $composerAutoloadFiles = $GLOBALS['__composer_autoload_files']; - if ( - !array_key_exists('e88992873b7765f9b5710cab95ba5dd7', $composerAutoloadFiles) - || !array_key_exists('3e76f7f02b41af8cea96018933f6b7e3', $composerAutoloadFiles) - ) { - echo "Composer autoloader changed\n"; - exit(1); + if ($composer !== null) { + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig(getcwd(), $composer); + } else { + $vendorDirectory = getcwd() . '/' . 'vendor'; } + $devOrPharLoader->unregister(); - // empty the global variable so that unprefixed functions from user-space can be loaded - $GLOBALS['__composer_autoload_files'] = [ - // fix unprefixed Hoa namespace - files already loaded - 'e88992873b7765f9b5710cab95ba5dd7' => true, - '3e76f7f02b41af8cea96018933f6b7e3' => true, - ]; + // fix missing key for vendor/hoa/protocol/Wrapper.php + $existingComposerAutoloadFiles = $GLOBALS['__composer_autoload_files'] ?? []; + $GLOBALS['__composer_autoload_files'] = array_merge( + $existingComposerAutoloadFiles, + array_fill_keys(['3e76f7f02b41af8cea96018933f6b7e3'], 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%'); - $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); - $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); + $composerAutoloaderProjectPaths = array_map(function(string $s): string { + return str_replace(DIRECTORY_SEPARATOR, '/', $s); + }, $composerAutoloaderProjectPaths); + $reversedComposerAutoloaderProjectPaths = array_values(array_unique(array_reverse($composerAutoloaderProjectPaths))); + + $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths, $analysisStartTime)); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new FixerWorkerCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new DumpParametersCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new DiagnoseCommand($reversedComposerAutoloaderProjectPaths)); $application->run(); })(); diff --git a/bin/transform-source.php b/bin/transform-source.php deleted file mode 100755 index 8ecbefb023..0000000000 --- a/bin/transform-source.php +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env php -type === null) { - return null; - } - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $node->type = null; - return $node; - } - - $node->setDocComment(new \PhpParser\Comment\Doc(sprintf('/** @var %s */', $this->printType($node->type)))); - $node->type = null; - - return $node; - } - - /** - * @param Identifier|Name|NullableType|UnionType $type - * @return string - */ - private function printType($type): string - { - if ($type instanceof NullableType) { - return $this->printType($type->type) . '|null'; - } - - if ($type instanceof UnionType) { - throw new \Exception('UnionType not yet supported'); - } - - if ($type instanceof Name) { - $name = $type->toString(); - if ($type->isFullyQualified()) { - return '\\' . $name; - } - - return $name; - } - - if ($type instanceof Identifier) { - return $type->name; - } - - throw new \Exception('Unsupported type class'); - } - -} - -(function () { - $dir = __DIR__ . '/../src'; - - $lexer = new Lexer\Emulative([ - 'usedAttributes' => [ - 'comments', - 'startLine', 'endLine', - 'startTokenPos', 'endTokenPos', - ], - ]); - $parser = new Parser\Php7($lexer, [ - 'useIdentifierNodes' => true, - 'useConsistentVariableNodes' => true, - 'useExpressionStatements' => true, - 'useNopStatements' => false, - ]); - $nameResolver = new NodeVisitor\NameResolver(null, [ - 'replaceNodes' => false - ]); - - $printer = new PrettyPrinter\Standard(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NodeVisitor\CloningVisitor()); - $traverser->addVisitor($nameResolver); - $traverser->addVisitor(new PhpPatcher($printer)); - - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir), - RecursiveIteratorIterator::LEAVES_ONLY - ); - foreach ($it as $file) { - $fileName = $file->getPathname(); - if (!preg_match('/\.php$/', $fileName)) { - continue; - } - - $code = \PHPStan\File\FileReader::read($fileName); - $origStmts = $parser->parse($code); - $newCode = $printer->printFormatPreserving( - $traverser->traverse($origStmts), - $origStmts, - $lexer->getTokens() - ); - - \PHPStan\File\FileWriter::write($fileName, $newCode); - } -})(); diff --git a/build-cs/composer.json b/build-cs/composer.json deleted file mode 100644 index 562c29c0f4..0000000000 --- a/build-cs/composer.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "require-dev": { - "consistence-community/coding-standard": "^3.11.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.3.0", - "squizlabs/php_codesniffer": "^3.5.3" - } -} diff --git a/build-cs/composer.lock b/build-cs/composer.lock deleted file mode 100644 index 19ea198bf0..0000000000 --- a/build-cs/composer.lock +++ /dev/null @@ -1,325 +0,0 @@ -{ - "_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": "2bd0f4453d373af44aff8ec8868f82e9", - "packages": [], - "packages-dev": [ - { - "name": "consistence-community/coding-standard", - "version": "3.11.0", - "source": { - "type": "git", - "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "20f5c3673013be606a62ba0b6624f5c0e43bb64e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/20f5c3673013be606a62ba0b6624f5c0e43bb64e", - "reference": "20f5c3673013be606a62ba0b6624f5c0e43bb64e", - "shasum": "" - }, - "require": { - "php": ">=7.4", - "slevomat/coding-standard": "~6.4", - "squizlabs/php_codesniffer": "~3.5.8" - }, - "replace": { - "consistence/coding-standard": "3.10.*" - }, - "require-dev": { - "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.2.0", - "phpunit/phpunit": "9.5.2" - }, - "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": { - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.0" - }, - "time": "2021-02-28T08:38:12+00:00" - }, - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.1", - "source": { - "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "fe390591e0241955f22eb9ba327d137e501c771c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", - "reference": "fe390591e0241955f22eb9ba327d137e501c771c", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" - }, - "require-dev": { - "composer/composer": "*", - "phpcompatibility/php-compatibility": "^9.0", - "sensiolabs/security-checker": "^4.1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "Dealerdirect\\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" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" - }, - "time": "2020-12-07T18:04:37+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "0.4.9", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/98a088b17966bdf6ee25c8a4b634df313d8aa531", - "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "consistence/coding-standard": "^3.5", - "ergebnis/composer-normalize": "^2.0.2", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "phing/phing": "^2.16.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.26", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^6.3", - "slevomat/coding-standard": "^4.7.2", - "symfony/process": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.4-dev" - } - }, - "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/master" - }, - "time": "2020-08-03T20:32:43+00:00" - }, - { - "name": "slevomat/coding-standard", - "version": "6.4.1", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "696dcca217d0c9da2c40d02731526c1e25b65346" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/696dcca217d0c9da2c40d02731526c1e25b65346", - "reference": "696dcca217d0c9da2c40d02731526c1e25b65346", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", - "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "0.4.5 - 0.4.9", - "squizlabs/php_codesniffer": "^3.5.6" - }, - "require-dev": { - "phing/phing": "2.16.3", - "php-parallel-lint/php-parallel-lint": "1.2.0", - "phpstan/phpstan": "0.12.48", - "phpstan/phpstan-deprecation-rules": "0.12.5", - "phpstan/phpstan-phpunit": "0.12.16", - "phpstan/phpstan-strict-rules": "0.12.5", - "phpunit/phpunit": "7.5.20|8.5.5|9.4.0" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "6.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.", - "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/6.4.1" - }, - "funding": [ - { - "url": "https://github.com/kukulich", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", - "type": "tidelift" - } - ], - "time": "2020-10-05T12:39:37+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.5.8", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", - "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" - ], - "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": "2020-10-23T02:01:07+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" -} diff --git a/build.xml b/build.xml deleted file mode 100644 index ba53184e43..0000000000 --- a/build.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/PHPStan/Build/AttributeNamedArgumentsRule.php b/build/PHPStan/Build/AttributeNamedArgumentsRule.php new file mode 100644 index 0000000000..fe520d007e --- /dev/null +++ b/build/PHPStan/Build/AttributeNamedArgumentsRule.php @@ -0,0 +1,86 @@ + + */ +final class AttributeNamedArgumentsRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Attribute::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $attributeName = $node->name->toString(); + if (!$this->reflectionProvider->hasClass($attributeName)) { + return []; + } + + $attributeReflection = $this->reflectionProvider->getClass($attributeName); + if (!$attributeReflection->hasConstructor()) { + return []; + } + $constructor = $attributeReflection->getConstructor(); + if (!$constructor->acceptsNamedArguments()->yes()) { + return []; + } + + $variants = $constructor->getVariants(); + if (count($variants) !== 1) { + return []; + } + + $parameters = $variants[0]->getParameters(); + + foreach ($node->args as $arg) { + if ($arg->name !== null) { + break; + } + + return [ + RuleErrorBuilder::message(sprintf('Attribute %s is not using named arguments.', $node->name->toString())) + ->identifier('phpstan.attributeWithoutNamedArguments') + ->nonIgnorable() + ->fixNode($node, static function (Node $node) use ($parameters) { + $args = $node->args; + foreach ($args as $i => $arg) { + if ($arg->name !== null) { + break; + } + + $parameterName = $parameters[$i]->getName(); + if ($parameterName === '') { + throw new ShouldNotHappenException(); + } + + $arg->name = new Node\Identifier($parameterName); + } + + return $node; + }) + ->build(), + ]; + } + + return []; + } + +} 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..85b313bb03 --- /dev/null +++ b/build/PHPStan/Build/FinalClassRule.php @@ -0,0 +1,84 @@ + + */ +final class FinalClassRule implements Rule +{ + + public function __construct(private FileHelper $fileHelper, private bool $skipTests = true) + { + } + + 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 ($this->skipTests && str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) { + return []; + } + + $errorBuilder = RuleErrorBuilder::message( + sprintf('Class %s must be abstract or final.', $classReflection->getDisplayName()), + )->identifier('phpstan.finalClass'); + + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof Node\Stmt\Class_ && $originalNode->name !== null) { + $errorBuilder->fixNode($originalNode, static function ($classNode) { + $classNode->flags |= Modifiers::FINAL; + + return $classNode; + }); + } + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/build/PHPStan/Build/MemoizationPropertyRule.php b/build/PHPStan/Build/MemoizationPropertyRule.php new file mode 100644 index 0000000000..7680c3eebe --- /dev/null +++ b/build/PHPStan/Build/MemoizationPropertyRule.php @@ -0,0 +1,150 @@ + + */ +final class MemoizationPropertyRule implements Rule +{ + + public function __construct(private FileHelper $fileHelper, private bool $skipTests = true) + { + } + + public function getNodeType(): string + { + return If_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $ifNode = $node; + + if (count($ifNode->stmts) !== 1 + || !$ifNode->stmts[0] instanceof Expression + || count($ifNode->elseifs) !== 0 + || $ifNode->else !== null + || !$ifNode->cond instanceof Identical + || !$this->isSupportedFetchNode($ifNode->cond->left) + || !$ifNode->cond->right instanceof ConstFetch + || strcasecmp($ifNode->cond->right->name->name, 'null') !== 0 + ) { + return []; + } + + $ifThenNode = $ifNode->stmts[0]->expr; + if (!$ifThenNode instanceof Assign || !$this->isSupportedFetchNode($ifThenNode->var)) { + return []; + } + + if ($this->areNodesNotEqual($ifNode->cond->left, [$ifThenNode->var])) { + return []; + } + + if ($this->skipTests && str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) { + return []; + } + + $errorBuilder = RuleErrorBuilder::message('This initializing if statement can be replaced with null coalescing assignment operator (??=).') + ->fixNode($node, static fn (If_ $node) => new Expression(new Coalesce($ifThenNode->var, $ifThenNode->expr))) + ->identifier('phpstan.memoizationProperty'); + + return [ + $errorBuilder->build(), + ]; + } + + /** + * @phpstan-assert-if-true PropertyFetch|StaticPropertyFetch $node + */ + private function isSupportedFetchNode(?Expr $node): bool + { + return $node instanceof PropertyFetch || $node instanceof StaticPropertyFetch; + } + + /** + * @param list $otherNodes + */ + private function areNodesNotEqual(PropertyFetch|StaticPropertyFetch $node, array $otherNodes): bool + { + if ($node instanceof PropertyFetch) { + if (!$node->var instanceof Variable + || !is_string($node->var->name) + || !$node->name instanceof Identifier + ) { + return true; + } + + foreach ($otherNodes as $otherNode) { + if (!$otherNode instanceof PropertyFetch) { + return true; + } + if (!$otherNode->var instanceof Variable + || !is_string($otherNode->var->name) + || !$otherNode->name instanceof Identifier + ) { + return true; + } + + if ($node->var->name !== $otherNode->var->name + || $node->name->name !== $otherNode->name->name + ) { + return true; + } + } + + return false; + } + + if (!$node->class instanceof Name || !$node->name instanceof VarLikeIdentifier) { + return true; + } + + foreach ($otherNodes as $otherNode) { + if (!$otherNode instanceof StaticPropertyFetch) { + return true; + } + + if (!$otherNode->class instanceof Name + || !$otherNode->name instanceof VarLikeIdentifier + ) { + return true; + } + + if ($node->class->toLowerString() !== $otherNode->class->toLowerString() + || $node->name->toString() !== $otherNode->name->toString() + ) { + return true; + } + } + + return false; + } + +} diff --git a/build/PHPStan/Build/NamedArgumentsRule.php b/build/PHPStan/Build/NamedArgumentsRule.php new file mode 100644 index 0000000000..b44a10ddea --- /dev/null +++ b/build/PHPStan/Build/NamedArgumentsRule.php @@ -0,0 +1,237 @@ + + */ +final class NamedArgumentsRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\CallLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsNamedArguments()) { + return []; + } + + if ($node->isFirstClassCallable()) { + return []; + } + + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + if ($this->reflectionProvider->hasFunction($node->name, $scope)) { + $function = $this->reflectionProvider->getFunction($node->name, $scope); + $variants = $function->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->processArgs($variants[0], $scope, $node); + } + } + + if ($node instanceof Node\Expr\New_ && $node->class instanceof Node\Name) { + if ($this->reflectionProvider->hasClass($node->class->toString())) { + $class = $this->reflectionProvider->getClass($node->class->toString()); + if ($class->hasConstructor()) { + $constructor = $class->getConstructor(); + $variants = $constructor->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->processArgs($variants[0], $scope, $node); + } + } + } + + if ($node instanceof Node\Expr\StaticCall && $node->class instanceof Node\Name && $node->name instanceof Node\Identifier) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $class = $this->reflectionProvider->getClass($className); + if ($class->hasNativeMethod($node->name->toString())) { + $method = $class->getNativeMethod($node->name->toString()); + $variants = $method->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->processArgs($variants[0], $scope, $node); + } + } + } + + return []; + } + + /** + * @return list + */ + private function processArgs(ExtendedParametersAcceptor $acceptor, Scope $scope, Node\Expr\FuncCall|Node\Expr\New_|Node\Expr\StaticCall $node): array + { + if ($acceptor->isVariadic()) { + return []; + } + $normalizedArgs = ArgumentsNormalizer::reorderArgs($acceptor, $node->getArgs()); + if ($normalizedArgs === null) { + return []; + } + + $hasNamedArgument = false; + foreach ($node->getArgs() as $arg) { + if ($arg->name === null) { + continue; + } + + $hasNamedArgument = true; + break; + } + + $errorBuilders = []; + $parameters = $acceptor->getParameters(); + $defaultValueWasPassed = []; + foreach ($normalizedArgs as $i => $normalizedArg) { + if ($normalizedArg->unpack) { + return []; + } + $parameter = $parameters[$i]; + if ($parameter->getDefaultValue() === null) { + continue; + } + if (!$parameter->passedByReference()->no()) { + continue; + } + $argValue = $scope->getType($normalizedArg->value); + if ($normalizedArg->name !== null) { + continue; + } + + /** @var Node\Arg|null $originalArg */ + $originalArg = $normalizedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + if ($originalArg === null) { + if ($hasNamedArgument) { + // this is an optional parameter not passed by the user, but filled in by ArgumentsNormalizer + continue; + } + } + + if (count($parameter->getDefaultValue()->getFiniteTypes()) === 0) { + continue; + } + + if (!$argValue->equals($parameter->getDefaultValue())) { + if (count($defaultValueWasPassed) > 0) { + $errorBuilders[] = RuleErrorBuilder::message(sprintf( + 'You\'re passing a non-default value %s to parameter $%s but previous %s (%s). You can skip %s and use named argument for $%s instead.', + $argValue->describe(VerbosityLevel::precise()), + $parameter->getName(), + count($defaultValueWasPassed) === 1 ? 'argument is passing default value to its parameter' : 'arguments are passing default values to their parameters', + implode(', ', $defaultValueWasPassed), + count($defaultValueWasPassed) === 1 ? 'it' : 'them', + $parameter->getName(), + )) + ->identifier('phpstan.namedArgument') + ->line($normalizedArg->getStartLine()) + ->nonIgnorable(); + } + continue; + } else { + if ($originalArg !== null && $originalArg->name !== null) { + $errorBuilders[] = RuleErrorBuilder::message(sprintf('Named argument $%s can be omitted, type %s is the same as the default value.', $originalArg->name, $argValue->describe(VerbosityLevel::precise()))) + ->identifier('phpstan.namedArgumentWithDefaultValue') + ->nonIgnorable(); + continue; + } + } + + $defaultValueWasPassed[] = '$' . $parameter->getName(); + } + + if (count($errorBuilders) > 0) { + $errorBuilders[0]->fixNode($node, static function ($node) use ($acceptor, $hasNamedArgument, $parameters, $scope) { + $normalizedArgs = ArgumentsNormalizer::reorderArgs($acceptor, $node->getArgs()); + if ($normalizedArgs === null) { + return $node; + } + + $newArgs = []; + $skippedOptional = false; + foreach ($normalizedArgs as $i => $normalizedArg) { + /** @var Node\Arg|null $originalArg */ + $originalArg = $normalizedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + if ($originalArg === null) { + if ($hasNamedArgument) { + // this is an optional parameter not passed by the user, but filled in by ArgumentsNormalizer + continue; + } + + $originalArg = $normalizedArg; + } + $parameter = $parameters[$i]; + if ($parameter->getDefaultValue() === null) { + $newArgs[] = $originalArg; + continue; + } + if (!$parameter->passedByReference()->no()) { + $newArgs[] = $originalArg; + continue; + } + if (count($parameter->getDefaultValue()->getFiniteTypes()) === 0) { + $newArgs[] = $originalArg; + continue; + } + $argValue = $scope->getType($normalizedArg->value); + if ($argValue->equals($parameter->getDefaultValue())) { + $skippedOptional = true; + continue; + } + + if ($skippedOptional) { + if ($parameter->getName() === '') { + throw new ShouldNotHappenException(); + } + + $newArgs[] = new Node\Arg($originalArg->value, $originalArg->byRef, $originalArg->unpack, $originalArg->getAttributes(), new Node\Identifier($parameter->getName())); + continue; + } + + $newArgs[] = $originalArg; + } + + $node->args = $newArgs; + + return $node; + }); + } + + return array_map(static fn ($builder) => $builder->build(), $errorBuilders); + } + +} diff --git a/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php b/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php new file mode 100644 index 0000000000..01762e05ca --- /dev/null +++ b/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php @@ -0,0 +1,162 @@ + + */ +final class OrChainIdenticalComparisonToInArrayRule implements Rule +{ + + public function __construct( + private ExprPrinter $printer, + private FileHelper $fileHelper, + private bool $skipTests = true, + ) + { + } + + public function getNodeType(): string + { + return If_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = $this->processConditionNode($node->cond, $scope); + foreach ($node->elseifs as $elseifCondNode) { + $errors = array_merge($errors, $this->processConditionNode($elseifCondNode->cond, $scope)); + } + + return $errors; + } + + /** + * @return list + */ + public function processConditionNode(Expr $condNode, Scope $scope): array + { + $comparisons = $this->unpackOrChain($condNode); + if (count($comparisons) < 2) { + return []; + } + + $firstComparison = array_shift($comparisons); + if (!$firstComparison instanceof Identical) { + return []; + } + + $subjectAndValue = $this->getSubjectAndValue($firstComparison); + if ($subjectAndValue === null) { + return []; + } + + if ($this->skipTests && str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) { + return []; + } + + $subjectNode = $subjectAndValue['subject']; + $subjectStr = $this->printer->printExpr($subjectNode); + $values = [$subjectAndValue['value']]; + + foreach ($comparisons as $comparison) { + if (!$comparison instanceof Identical) { + return []; + } + + $currentSubjectAndValue = $this->getSubjectAndValue($comparison); + if ($currentSubjectAndValue === null) { + return []; + } + + if ($this->printer->printExpr($currentSubjectAndValue['subject']) !== $subjectStr) { + return []; + } + + $values[] = $currentSubjectAndValue['value']; + } + + $errorBuilder = RuleErrorBuilder::message('This chain of identical comparisons can be simplified using in_array().') + ->line($condNode->getStartLine()) + ->fixNode($condNode, static fn (Expr $node) => self::createInArrayCall($subjectNode, $values)) + ->identifier('or.chainIdenticalComparison'); + + return [$errorBuilder->build()]; + } + + /** + * @return list + */ + private function unpackOrChain(Expr $node): array + { + if ($node instanceof BooleanOr) { + return [...$this->unpackOrChain($node->left), ...$this->unpackOrChain($node->right)]; + } + + return [$node]; + } + + /** + * @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node + */ + private static function isSubjectNode(Expr $node): bool + { + return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch; + } + + /** + * @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null + */ + private function getSubjectAndValue(Identical $comparison): ?array + { + if (self::isSubjectNode($comparison->left) && !self::isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } + + /** + * @param list $values + */ + private static function createInArrayCall(Expr $subjectNode, array $values): FuncCall + { + return new FuncCall(new Name('\in_array'), [ + new Arg($subjectNode), + new Arg(new Array_(array_map(static fn ($value) => new ArrayItem($value), $values))), + new Arg(new ConstFetch(new Name('true'))), + ]); + } + +} diff --git a/build/PHPStan/Build/OverrideAttributeThirdPartyMethodRule.php b/build/PHPStan/Build/OverrideAttributeThirdPartyMethodRule.php new file mode 100644 index 0000000000..3249fab3e1 --- /dev/null +++ b/build/PHPStan/Build/OverrideAttributeThirdPartyMethodRule.php @@ -0,0 +1,92 @@ + + */ +final class OverrideAttributeThirdPartyMethodRule implements Rule +{ + + public function __construct( + private PhpVersion $phpVersion, + private MethodPrototypeFinder $methodPrototypeFinder, + ) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); + if ($prototypeData === null) { + return []; + } + + [$prototype, $prototypeDeclaringClass] = $prototypeData; + + if (str_starts_with($prototypeDeclaringClass->getName(), 'PHPStan\\')) { + if ( + !str_starts_with($prototypeDeclaringClass->getName(), 'PHPStan\\PhpDocParser\\') + && !str_starts_with($prototypeDeclaringClass->getName(), 'PHPStan\\BetterReflection\\') + ) { + return []; + } + } + + $messages = []; + if ( + $this->phpVersion->supportsOverrideAttribute() + && !$scope->isInTrait() + && !$this->hasOverrideAttribute($node->getOriginalNode()) + ) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides 3rd party method %s::%s() but is missing the #[\Override] attribute.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->identifier('phpstan.missing3rdPartyOverride') + ->fixNode($node->getOriginalNode(), static function (Node\Stmt\ClassMethod $method) { + $method->attrGroups[] = new Node\AttributeGroup([ + new Attribute(new Node\Name\FullyQualified('Override')), + ]); + + return $method; + }) + ->build(); + } + + return $messages; + } + + 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; + } + +} diff --git a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php index 1a1207e723..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 @@ -31,22 +28,23 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { if (count($methodCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $argType = $scope->getType($methodCall->getArgs()[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + 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 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/PHPStan/Build/SkipTestsWithRequiresPhpAttributeRule.php b/build/PHPStan/Build/SkipTestsWithRequiresPhpAttributeRule.php new file mode 100644 index 0000000000..05de1553e2 --- /dev/null +++ b/build/PHPStan/Build/SkipTestsWithRequiresPhpAttributeRule.php @@ -0,0 +1,130 @@ + + */ +final class SkipTestsWithRequiresPhpAttributeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methodReflection = $node->getMethodReflection(); + if (!$methodReflection->getDeclaringClass()->is(TestCase::class)) { + return []; + } + + $originalNode = $node->getOriginalNode(); + if ($originalNode->stmts === null) { + return []; + } + + if (count($originalNode->stmts) === 0) { + return []; + } + + $firstStmt = $originalNode->stmts[0]; + if (!$firstStmt instanceof Node\Stmt\If_) { + return []; + } + + if (!$firstStmt->cond instanceof Node\Expr\BinaryOp) { + return []; + } + + switch (get_class($firstStmt->cond)) { + case Node\Expr\BinaryOp\SmallerOrEqual::class: + $inverseBinaryOpSigil = '>'; + break; + case Node\Expr\BinaryOp\Smaller::class: + $inverseBinaryOpSigil = '>='; + break; + case Node\Expr\BinaryOp\GreaterOrEqual::class: + $inverseBinaryOpSigil = '<'; + break; + case Node\Expr\BinaryOp\Greater::class: + $inverseBinaryOpSigil = '<='; + break; + case Node\Expr\BinaryOp\Identical::class: + $inverseBinaryOpSigil = '!=='; + break; + case Node\Expr\BinaryOp\NotIdentical::class: + $inverseBinaryOpSigil = '==='; + break; + default: + throw new ShouldNotHappenException('No inverse comparison specified for ' . get_class($firstStmt->cond)); + } + + if (!$firstStmt->cond->left instanceof Node\Expr\ConstFetch || $firstStmt->cond->left->name->toString() !== 'PHP_VERSION_ID') { + return []; + } + + if (!$firstStmt->cond->right instanceof Node\Scalar\Int_) { + return []; + } + + if (count($firstStmt->stmts) !== 1) { + return []; + } + + $ifStmt = $firstStmt->stmts[0]; + if (!$ifStmt instanceof Node\Stmt\Expression) { + return []; + } + + if (!$ifStmt->expr instanceof Node\Expr\StaticCall && !$ifStmt->expr instanceof Node\Expr\MethodCall) { + return []; + } + + if (!$ifStmt->expr->name instanceof Node\Identifier || $ifStmt->expr->name->toLowerString() !== 'marktestskipped') { + return []; + } + + $phpVersion = new PhpVersion($firstStmt->cond->right->value); + + return [ + RuleErrorBuilder::message('Skip tests with #[RequiresPhp] attribute instead.') + ->identifier('phpstan.skipTestsRequiresPhp') + ->line($firstStmt->getStartLine()) + ->fixNode($originalNode, static function (Node\Stmt\ClassMethod $node) use ($phpVersion, $inverseBinaryOpSigil) { + $stmts = $node->stmts; + if ($stmts === null) { + return $node; + } + + unset($stmts[0]); + $node->stmts = array_values($stmts); + $node->attrGroups[] = new Node\AttributeGroup([ + new Attribute(new Node\Name\FullyQualified('PHPUnit\\Framework\\Attributes\\RequiresPhp'), [ + new Node\Arg(new Node\Scalar\String_(sprintf('%s %s', $inverseBinaryOpSigil, $phpVersion->getVersionString()))), + ]), + ]); + + return $node; + }) + ->build(), + ]; + } + + +} diff --git a/build/baseline-32bit.neon b/build/baseline-32bit.neon new file mode 100644 index 0000000000..82adbae209 --- /dev/null +++ b/build/baseline-32bit.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$value of class PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType constructor expects int, float given\\.$#" + count: 2 + path: ../src/Analyser/MutatingScope.php diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index db957bbc2f..b28b9f9f5d 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -4,10 +4,6 @@ parameters: message: "#^Class PHPStan\\\\Command\\\\ErrorsConsoleStyle has an uninitialized property \\$progressBar\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Command/ErrorsConsoleStyle.php - - - message: "#^Class PHPStan\\\\DependencyInjection\\\\Reflection\\\\DirectClassReflectionExtensionRegistryProvider has an uninitialized property \\$broker\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php - message: "#^Class PHPStan\\\\Parallel\\\\ParallelAnalyser has an uninitialized property \\$processPool\\. Give it default value or assign it in the constructor\\.$#" @@ -18,10 +14,6 @@ parameters: message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$process\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Parallel/Process.php - - - message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$in\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Parallel/Process.php - message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodes\\. Give it default value or assign it in the constructor\\.$#" count: 1 @@ -55,11 +47,22 @@ parameters: message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodeResolver\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/PhpDoc/ResolvedPhpDocBlock.php + + - + message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/PhpDoc/ResolvedPhpDocBlock.php + - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$fileName\\. Give it default value or assign it in the constructor\\.$#" count: 1 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 @@ -74,7 +77,3 @@ parameters: 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 index b9be86042c..59bcf6e715 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -1,42 +1,37 @@ parameters: ignoreErrors: - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 path: ../src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: '#^Call to function method_exists\(\) with ReflectionFunction and ''getClosureCalledCla…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php + path: ../src/Type/ClosureTypeFactory.php - - message: "#^Strict comparison using \\=\\=\\= between array&nonEmpty and false will always evaluate to false\\.$#" + message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and 'isPromoted' will always evaluate to true\\.$#" - paths: - - ../src/Reflection/Php/PhpClassReflectionExtension.php - - ../src/Reflection/Php/PhpPropertyReflection.php - - - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and 'getDefaultValue' will always evaluate to true\\.$#" - paths: - - ../tests/PHPStan/Analyser/AnalyserIntegrationTest.php + path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - - message: "#^Call to function method_exists\\(\\) with ReflectionParameter and 'isPromoted' will always evaluate to true\\.$#" - paths: - - ../src/Reflection/Php/PhpClassReflectionExtension.php + message: '#^Strict comparison using \=\=\= between int\<0, max\> and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: "#^Call to function method_exists\\(\\) with ReflectionClass and 'getAttributes' will always evaluate to true\\.$#" + message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 - path: ../src/Reflection/ClassReflection.php + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php diff --git a/build/baseline-8.1.neon b/build/baseline-8.1.neon new file mode 100644 index 0000000000..aab4991158 --- /dev/null +++ b/build/baseline-8.1.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/build/baseline-lt-7.3.neon b/build/baseline-lt-7.3.neon deleted file mode 100644 index 8d088b9056..0000000000 --- a/build/baseline-lt-7.3.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Call to an undefined static method PHPUnit\\\\Framework\\\\TestCase\\:\\:assertFileDoesNotExist\\(\\)\\.$#" - count: 1 - path: ../src/Testing/LevelsTestCase.php diff --git a/build/baseline-pre-8.0.neon b/build/baseline-pre-8.0.neon new file mode 100644 index 0000000000..237a55439c --- /dev/null +++ b/build/baseline-pre-8.0.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function method_exists\(\) with ReflectionFunction and ''getClosureCalledCla…'' will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: ../src/Type/ClosureTypeFactory.php diff --git a/build/collision-detector.json b/build/collision-detector.json new file mode 100644 index 0000000000..c5171fcc96 --- /dev/null +++ b/build/collision-detector.json @@ -0,0 +1,21 @@ +{ + "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", + "../tests/PHPStan/Rules/Cast/data/void-cast.php" + ] +} diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php new file mode 100644 index 0000000000..9819b8d3df --- /dev/null +++ b/build/composer-dependency-analyser.php @@ -0,0 +1,39 @@ +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 45fb391f8d..0000000000 --- a/build/composer-require-checker.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "symbol-whitelist" : [ - "null", "true", "false", - "static", "self", "parent", - "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", - "PHPUnit\\Framework\\TestCase", "PHPUnit\\Framework\\AssertionFailedError", "Composer\\Autoload\\ClassLoader", - "JSON_THROW_ON_ERROR", "JSON_INVALID_UTF8_IGNORE", "JsonSerializable", "SimpleXMLElement", "PHPStan\\ExtensionInstaller\\GeneratedConfig", "Nette\\DI\\InvalidConfigurationException", - "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_ENCODED", "FILTER_SANITIZE_MAGIC_QUOTES", "FILTER_SANITIZE_NUMBER_FLOAT", - "FILTER_SANITIZE_NUMBER_INT", "FILTER_SANITIZE_SPECIAL_CHARS", "FILTER_SANITIZE_STRING", "FILTER_SANITIZE_URL", "FILTER_VALIDATE_BOOLEAN", - "FILTER_VALIDATE_EMAIL", "FILTER_VALIDATE_FLOAT", "FILTER_VALIDATE_INT", "FILTER_VALIDATE_IP", "FILTER_VALIDATE_MAC", "FILTER_VALIDATE_REGEXP", - "FILTER_VALIDATE_URL", "FILTER_NULL_ON_FAILURE", "FILTER_FORCE_ARRAY", "FILTER_SANITIZE_ADD_SLASHES", "FILTER_DEFAULT", "FILTER_UNSAFE_RAW", "opcache_invalidate", "ValueError", "ReflectionUnionType", "Attribute", - "Clue\\React\\Block\\await" - ], - "php-core-extensions" : [ - "Core", - "date", - "pcre", - "Phar", - "Reflection", - "SPL", - "standard", - "pcntl", - "mbstring", - "hash", - "tokenizer", - "dom" - ] -} diff --git a/build/composer-require-checker.phar b/build/composer-require-checker.phar deleted file mode 100755 index 59c029ef5c..0000000000 Binary files a/build/composer-require-checker.phar and /dev/null differ diff --git a/build/datetime-php-83.neon b/build/datetime-php-83.neon new file mode 100644 index 0000000000..953379bf4f --- /dev/null +++ b/build/datetime-php-83.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^If condition is always false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between DateTime and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php diff --git a/build/deprecated-8.4.neon b/build/deprecated-8.4.neon new file mode 100644 index 0000000000..6b3bd6db5e --- /dev/null +++ b/build/deprecated-8.4.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Use of constant E_STRICT is deprecated\.$#' + identifier: constant.deprecated + count: 1 + path: ../src/Analyser/FileAnalyser.php diff --git a/build/downgrade.php b/build/downgrade.php new file mode 100644 index 0000000000..437bfcf1cc --- /dev/null +++ b/build/downgrade.php @@ -0,0 +1,21 @@ + __DIR__ . '/../composer.json', + 'paths' => [ + __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..4b2208f5fe --- /dev/null +++ b/build/ignore-by-architecture.neon.php @@ -0,0 +1,12 @@ += 80000) { + $includes[] = __DIR__ . '/baseline-8.0.neon'; +} else { + $includes[] = __DIR__ . '/baseline-pre-8.0.neon'; +} +if (PHP_VERSION_ID >= 80100) { + $includes[] = __DIR__ . '/baseline-8.1.neon'; +} else { + $includes[] = __DIR__ . '/enums.neon'; + $includes[] = __DIR__ . '/readonly-property.neon'; +} -$adapter = new NeonAdapter(); +if (PHP_VERSION_ID >= 70400) { + $includes[] = __DIR__ . '/ignore-gte-php7.4-errors.neon'; +} -$config = []; -if (PHP_VERSION_ID < 70300) { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/baseline-lt-7.3.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 { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/baseline-7.3.neon')); + $includes[] = __DIR__ . '/spl-autoload-functions-php-8.neon'; } -if (PHP_VERSION_ID >= 80000) { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/baseline-8.0.neon')); + +if (PHP_VERSION_ID >= 80300) { + $includes[] = __DIR__ . '/datetime-php-83.neon'; } -if (PHP_VERSION_ID >= 70400) { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/ignore-gte-php7.4-errors.neon')); +if (PHP_VERSION_ID >= 80400) { + $includes[] = __DIR__ . '/deprecated-8.4.neon'; } +if (PHP_VERSION_ID < 80200) { + $includes[] = __DIR__ . '/old-phpunit.neon'; +} else { + $includes[] = __DIR__ . '/new-phpunit.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 cae6484551..112fb17e4c 100644 --- a/build/ignore-gte-php7.4-errors.neon +++ b/build/ignore-gte-php7.4-errors.neon @@ -3,8 +3,8 @@ includes: parameters: ignoreErrors: - - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and '(?:hasType|getType)' will always evaluate to true\\.$#" - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - '#^Class PHPStan\\Rules\\RuleErrors\\RuleError(?:\d+) has an uninitialized property (?:\$message|\$line|\$identifier|\$tip|\$file|\$metadata)#' + - '#^Class PHPStan\\Rules\\RuleErrors\\RuleError(?:\d+) has an uninitialized property (?:\$message|\$line|\$identifier|\$tip|\$file|\$metadata|\$originalNode)#' - '#Extension has an uninitialized property (?:\$typeSpecifier|\$broker)#' + - + message: '#has an uninitialized property#' + path: ../tests diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon new file mode 100644 index 0000000000..34f9e1f598 --- /dev/null +++ b/build/more-enum-adapter-errors.neon @@ -0,0 +1,28 @@ +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\\.$#" + - + rawMessage: 'Asserted type PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum for $this->getNativeReflection() with type PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass can never happen.' + count: 1 + path: ../src/Reflection/ClassReflection.php diff --git a/build/new-phpunit.neon b/build/new-phpunit.neon new file mode 100644 index 0000000000..b94c623841 --- /dev/null +++ b/build/new-phpunit.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined static method PHPUnit\\Framework\\TestCase\:\:assertFileNotExists\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: ../src/Testing/LevelsTestCase.php diff --git a/build/old-phpunit.neon b/build/old-phpunit.neon new file mode 100644 index 0000000000..52e9beb14d --- /dev/null +++ b/build/old-phpunit.neon @@ -0,0 +1,37 @@ +parameters: + excludePaths: + - ../src/Testing/PHPUnit/* + + ignoreErrors: + - + message: '#^Instanceof references internal interface PHPUnit\\Exception\.$#' + identifier: instanceof.internalInterface + count: 1 + path: ../tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + - + message: ''' + #^Call to deprecated method assertFileNotExists\(\) of class PHPUnit\\Framework\\Assert\: + https\://github\.com/sebastianbergmann/phpunit/issues/4077$# + ''' + identifier: staticMethod.deprecated + count: 1 + path: ../src/Testing/LevelsTestCase.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\ExpectationFailedException\.$#' + identifier: catch.internalClass + count: 1 + path: ../src/Testing/PHPStanTestCase.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 diff --git a/build/phpstan.neon b/build/phpstan.neon index 8d5e8fb692..017fd4f8fa 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -1,13 +1,16 @@ includes: - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon - ../vendor/phpstan/phpstan-nette/rules.neon - - ../vendor/phpstan/phpstan-php-parser/extension.neon - ../vendor/phpstan/phpstan-phpunit/extension.neon - ../vendor/phpstan/phpstan-phpunit/rules.neon - ../vendor/phpstan/phpstan-strict-rules/rules.neon + - ../vendor/shipmonk/dead-code-detector/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: @@ -16,19 +19,19 @@ parameters: - ../tests bootstrapFiles: - ../tests/phpstan-bootstrap.php + cache: + nodesByStringCountMax: 128 checkUninitializedProperties: true checkMissingCallableSignature: true excludePaths: - - ../src/Reflection/SignatureMap/functionMap.php - - ../src/Reflection/SignatureMap/functionMetadata.php - ../tests/*/data/* - ../tests/tmp/* + - ../tests/vendor/* + - ../tests/PHPStan/Analyser/nsrt/* - ../tests/PHPStan/Analyser/traits/* - ../tests/notAutoloaded/* - - ../tests/PHPStan/Generics/functions.php - ../tests/PHPStan/Reflection/UnionTypesTest.php - ../tests/PHPStan/Reflection/MixedTypeTest.php - - ../tests/PHPStan/Reflection/StaticTypeTest.php - ../tests/e2e/magic-setter/* - ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -42,6 +45,7 @@ parameters: - 'Symfony\Component\Finder\Exception\DirectoryNotFoundException' - 'InvalidArgumentException' - 'PHPStan\DependencyInjection\ParameterNotFoundException' + - 'PHPStan\DependencyInjection\DuplicateIncludedFilesException' - 'PHPStan\Analyser\UndefinedVariableException' - 'RuntimeException' - 'Nette\Neon\Exception' @@ -55,13 +59,14 @@ parameters: - 'PHPStan\Broker\ClassNotFoundException' - 'PHPStan\Broker\FunctionNotFoundException' - 'PHPStan\Broker\ConstantNotFoundException' + - 'PHPStan\DependencyInjection\MissingServiceException' - 'PHPStan\Reflection\MissingMethodFromReflectionException' - 'PHPStan\Reflection\MissingPropertyFromReflectionException' - 'PHPStan\Reflection\MissingConstantFromReflectionException' - 'PHPStan\Type\CircularTypeAliasDefinitionException' - - 'PHPStan\Broker\ClassAutoloadingException' + - 'PHPStan\Reflection\MissingStaticAccessorInstanceException' - 'LogicException' - - 'TypeError' + - 'Error' check: missingCheckedExceptionInThrows: true tooWideThrowType: true @@ -71,50 +76,70 @@ parameters: - '#Variable property access on PhpParser\\Node#' - '#Test::data[a-zA-Z0-9_]+\(\) return type has no value type specified in iterable type#' - - message: '#Fetching class constant class of deprecated class DeprecatedAnnotations\\DeprecatedFoo.#' + identifier: shipmonk.deadMethod + message: '#^Unused .*?Factory::create#' # likely used in DIC + - + identifier: shipmonk.deadMethod + paths: + - ../tests/PHPStan/Tests + - ../tests/e2e + - + identifier: shipmonk.deadConstant + paths: + - ../tests/PHPStan/Fixture + reportUnmatched: false # constants on enums, not reported on PHP8- + - + identifier: shipmonk.deadMethod + path: ../src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php + - + message: ''' + #^Access to constant on deprecated class DeprecatedAnnotations\\DeprecatedFoo\: + in 1\.0\.0\.$# + ''' path: ../tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php - - message: '#Fetching class constant class of deprecated class DeprecatedAnnotations\\DeprecatedWithMultipleTags.#' + message: ''' + #^Access to constant on deprecated class DeprecatedAnnotations\\DeprecatedWithMultipleTags\: + in Foo 1\.1\.0 and will be removed in 1\.5\.0, use + \\Foo\\Bar\\NotDeprecated instead\.$# + ''' path: ../tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php - - message: '#^Variable property access on PHPStan\\Rules\\RuleError\.$#' + message: '#^Variable property access on T of PHPStan\\Rules\\RuleError\.$#' path: ../src/Rules/RuleErrorBuilder.php - message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" count: 1 path: ../src/Command/CommandHelper.php + - + message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" + count: 1 + path: ../src/Diagnose/PHPStanDiagnoseExtension.php + - '#^Short ternary operator is not allowed#' reportStaticMethodSignatures: true tmpDir: %rootDir%/tmp stubFiles: - stubs/ReactChildProcess.stub - stubs/ReactStreams.stub + - stubs/NetteDIContainer.stub + - stubs/PhpParserName.stub + - stubs/Identifier.stub + +rules: + - PHPStan\Build\FinalClassRule + - PHPStan\Build\AttributeNamedArgumentsRule + - PHPStan\Build\NamedArgumentsRule + - PHPStan\Build\OverrideAttributeThirdPartyMethodRule + - PHPStan\Build\SkipTestsWithRequiresPhpAttributeRule + - PHPStan\Build\MemoizationPropertyRule + - PHPStan\Build\OrChainIdenticalComparisonToInArrayRule + services: - class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Internal\ContainerDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Internal\UnionTypeGetInternalDynamicReturnTypeExtension + class: PHPStan\Build\ContainerDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/build/readonly-property.neon b/build/readonly-property.neon new file mode 100644 index 0000000000..96657fb795 --- /dev/null +++ b/build/readonly-property.neon @@ -0,0 +1,3 @@ +parameters: + excludePaths: + - ../tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php diff --git a/build/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/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..bab56d4d34 --- /dev/null +++ b/changelog-generator/run.php @@ -0,0 +1,160 @@ +#!/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 is:pull-request', $commit['hash'])); + $issues = $searchApi->issues(sprintf('repo:phpstan/phpstan %s is:issue', $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'])) { + if ($responseItem['number'] === 13191) { + continue; + } + $parenthesis = sprintf('[#%d](%s)', $responseItem['number'], 'https://github.com/phpstan/phpstan-src/pull/' . $responseItem['number']); + $thanks = $responseItem['user']['login']; + } else { + $issuesToReference[] = sprintf('#%d', $responseItem['number']); + } + } + + $output->writeln(sprintf('* %s (%s)%s%s', $commit['message'], $parenthesis, count($issuesToReference) > 0 ? ', ' . implode(', ', $issuesToReference) : '', $thanks !== null ? sprintf(', thanks @%s!', $thanks) : '')); + } + + return 0; + } + + /** + * @param string[] $commandParts + */ + private function exec(array $commandParts): string + { + $command = implode(' ', array_map(static fn (string $part): string => escapeshellarg($part), $commandParts)); + + exec($command, $outputLines, $statusCode); + $output = implode("\n", $outputLines); + if ($statusCode !== 0) { + throw new InvalidArgumentException(sprintf('Command %s failed: %s', $command, $output)); + } + + return $output; + } + + }; + + $application = new Application(); + $application->add($command); + $application->setDefaultCommand('run', true); + $application->setCatchExceptions(false); + $application->run(); +})(); diff --git a/changelog-generator/src/RateLimitPlugin.php b/changelog-generator/src/RateLimitPlugin.php new file mode 100644 index 0000000000..fd724ab31f --- /dev/null +++ b/changelog-generator/src/RateLimitPlugin.php @@ -0,0 +1,47 @@ +client = $client; + } + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $path = $request->getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + /** @var RateLimit $api */ + $api = $this->client->api('rate_limit'); + + /** @var RateLimitResource $resource */ + $resource = $api->getResource('search'); + if ($resource->getRemaining() < 10) { + $reset = $resource->getReset(); + $sleepFor = $reset - time(); + if ($sleepFor > 0) { + sleep($sleepFor); + } + } + + return $next($request); + } + +} diff --git a/compiler/README.md b/compiler/README.md index 83e480cb3a..36d0676d9e 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -4,7 +4,9 @@ ```bash composer install -php bin/compile +php bin/prepare +cd build +php ../box/vendor/bin/box compile --no-parallel ``` The compiled PHAR will be in `tmp/phpstan.phar`. 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/box/.gitignore b/compiler/box/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/compiler/box/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/compiler/box/composer.json b/compiler/box/composer.json new file mode 100644 index 0000000000..4ea8c3e23c --- /dev/null +++ b/compiler/box/composer.json @@ -0,0 +1,23 @@ +{ + "require": { + "humbug/box": "^4.6", + "cweagans/composer-patches": "^1.7" + }, + "config": { + "platform": { + "php": "8.2.99" + }, + "allow-plugins": { + "vaimo/composer-patches": true, + "cweagans/composer-patches": true + } + }, + "extra": { + "composer-exit-on-patch-failure": true, + "patches": { + "humbug/php-scoper": [ + "patches/ScoperAutoloaderGenerator.patch" + ] + } + } +} diff --git a/compiler/box/composer.lock b/compiler/box/composer.lock new file mode 100644 index 0000000000..a4695f7f3f --- /dev/null +++ b/compiler/box/composer.lock @@ -0,0 +1,4171 @@ +{ + "_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": "fb82d3fddd187abc2f321b38688fb441", + "packages": [ + { + "name": "amphp/amp", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-26T16:07:39+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-12-21T01:56:09+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "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-11-12T16:29:46+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": "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": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "fidry/console", + "version": "0.6.11", + "source": { + "type": "git", + "url": "https://github.com/theofidry/console.git", + "reference": "bea8316beae874fc5b8be679d67dd3169c7e205f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/console/zipball/bea8316beae874fc5b8be679d67dd3169c7e205f", + "reference": "bea8316beae874fc5b8be679d67dd3169c7e205f", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/console": "^6.4 || ^7.2", + "symfony/deprecation-contracts": "^3.4", + "symfony/event-dispatcher-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php84": "^1.31", + "symfony/service-contracts": "^2.5 || ^3.0", + "thecodingmachine/safe": "^2.0 || ^3.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "symfony/dependency-injection": "<6.4.0 || >=7.0.0 <7.2.0", + "symfony/framework-bundle": "<6.4.0 || >=7.0.0 <7.2.0", + "symfony/http-kernel": "<6.4.0 || >=7.0.0 <7.2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "composer/semver": "^3.3.2", + "ergebnis/composer-normalize": "^2.33", + "fidry/makefile": "^0.2.1 || ^1.0.0", + "infection/infection": "^0.28", + "phpunit/phpunit": "^10.2", + "symfony/dependency-injection": "^6.4 || ^7.2", + "symfony/flex": "^2.4.0", + "symfony/framework-bundle": "^6.4 || ^7.2", + "symfony/http-kernel": "^6.4 || ^7.2", + "symfony/yaml": "^6.4 || ^7.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Library to create CLI applications", + "keywords": [ + "cli", + "console", + "symfony" + ], + "support": { + "issues": "https://github.com/theofidry/console/issues", + "source": "https://github.com/theofidry/console/tree/0.6.11" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-02-14T11:06:15+00:00" + }, + { + "name": "fidry/filesystem", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theofidry/filesystem.git", + "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/3e1f9cac40f807b7c4196013ab77cc1b9416e3e5", + "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/filesystem": "^6.4 || ^7.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": ">=0.26", + "phpunit/phpunit": "^10.3", + "symfony/finder": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\FileSystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Symfony Filesystem with a few more utilities.", + "keywords": [ + "filesystem" + ], + "support": { + "issues": "https://github.com/theofidry/filesystem/issues", + "source": "https://github.com/theofidry/filesystem/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-02-13T22:58:51+00:00" + }, + { + "name": "humbug/box", + "version": "4.6.7", + "source": { + "type": "git", + "url": "https://github.com/box-project/box.git", + "reference": "190d52718c2876452625c4489a04771e0f22c06b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/box-project/box/zipball/190d52718c2876452625c4489a04771e0f22c06b", + "reference": "190d52718c2876452625c4489a04771e0f22c06b", + "shasum": "" + }, + "require": { + "amphp/parallel": "^2.0", + "composer-plugin-api": "^2.2", + "composer/semver": "^3.3.2", + "composer/xdebug-handler": "^3.0.3", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-phar": "*", + "fidry/console": "^0.6.0", + "fidry/filesystem": "^1.2.1", + "humbug/php-scoper": "^0.18.14", + "justinrainbow/json-schema": "^6.2.0", + "nikic/iter": "^2.2", + "php": "^8.2", + "phpdocumentor/reflection-docblock": "^5.4", + "phpdocumentor/type-resolver": "^1.7", + "psr/log": "^3.0", + "sebastian/diff": "^5.0 || ^6.0 || ^7.0", + "seld/jsonlint": "^1.10.2", + "seld/phar-utils": "^1.2", + "symfony/finder": "^6.4.0 || ^7.0.0", + "symfony/polyfill-iconv": "^1.28", + "symfony/polyfill-mbstring": "^1.28", + "symfony/process": "^6.4.0 || ^7.0.0", + "symfony/var-dumper": "^6.4.0 || ^7.0.0", + "thecodingmachine/safe": "^2.5 || ^3.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "marc-mabe/php-enum": "<4.4" + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ergebnis/composer-normalize": "^2.29", + "ext-xml": "*", + "fidry/makefile": "^1.0.1", + "mikey179/vfsstream": "^1.6.11", + "phpspec/prophecy": "^1.18", + "phpspec/prophecy-phpunit": "^2.1.0", + "phpunit/phpunit": "^10.5.2", + "symfony/yaml": "^6.4.0 || ^7.0.0" + }, + "suggest": { + "ext-openssl": "To accelerate private key generation." + }, + "bin": [ + "bin/box" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "KevinGH\\Box\\": "src" + }, + "exclude-from-classmap": [ + "/Test/", + "vendor/humbug/php-scoper/vendor-hotfix" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Fast, zero config application bundler with PHARs.", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/box-project/box/issues", + "source": "https://github.com/box-project/box/tree/4.6.7" + }, + "time": "2025-09-06T15:32:16+00:00" + }, + { + "name": "humbug/php-scoper", + "version": "0.18.17", + "source": { + "type": "git", + "url": "https://github.com/humbug/php-scoper.git", + "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/0a2556c7c23776a61cf22689e2f24298ba00e33a", + "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a", + "shasum": "" + }, + "require": { + "fidry/console": "^0.6.10", + "fidry/filesystem": "^1.1", + "jetbrains/phpstorm-stubs": "^2024.1", + "nikic/php-parser": "^5.0", + "php": "^8.2", + "symfony/console": "^6.4 || ^7.0", + "symfony/filesystem": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/var-dumper": "^7.1", + "thecodingmachine/safe": "^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "ergebnis/composer-normalize": "^2.28", + "fidry/makefile": "^1.0", + "humbug/box": "^4.6.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "bin": [ + "bin/php-scoper" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Humbug\\PhpScoper\\": "src/" + }, + "classmap": [ + "vendor-hotfix/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + }, + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com" + } + ], + "description": "Prefixes all PHP namespaces in a file or directory.", + "support": { + "issues": "https://github.com/humbug/php-scoper/issues", + "source": "https://github.com/humbug/php-scoper/tree/0.18.17" + }, + "time": "2025-02-19T22:50:39+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2024.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "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" + }, + "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/v2024.3" + }, + "time": "2024-12-14T08:03:12+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.5.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "b5ab21e431594897e5bb86343c01f140ba862c26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/b5ab21e431594897e5bb86343c01f140ba862c26", + "reference": "b5ab21e431594897e5bb86343c01f140ba862c26", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.1" + }, + "time": "2025-08-29T10:58:11+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, + { + "name": "nikic/iter", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/iter.git", + "reference": "3f031ae08d82c4394410e76b88b441331a6fa15f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/iter/zipball/3f031ae08d82c4394410e76b88b441331a6fa15f", + "reference": "3f031ae08d82c4394410e76b88b441331a6fa15f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^4.18 || ^5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/iter.func.php", + "src/iter.php", + "src/iter.rewindable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Iteration primitives using generators", + "keywords": [ + "functional", + "generator", + "iterator" + ], + "support": { + "issues": "https://github.com/nikic/iter/issues", + "source": "https://github.com/nikic/iter/tree/v2.4.1" + }, + "time": "2024-03-19T20:45:05+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "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.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+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": "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-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": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-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", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2024-07-11T14:55:45+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": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^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/v7.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": "2025-05-24T10:34:04+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "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/v7.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": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^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/v7.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": "2024-12-30T19:00:26+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.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.32.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-iconv", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa", + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + } + }, + "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 Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.32.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-17T14:58:18+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.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.32.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.32.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.32.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.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "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.32.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-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "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\\Php84\\": "" + }, + "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.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.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": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "shasum": "" + }, + "require": { + "php": ">=8.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/v7.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": "2025-04-17T09:11:12+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "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": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "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.6.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": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "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.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": "2025-04-20T20:19:01+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "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 mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.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": "2025-04-27T18:39:23+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-05-14T06:15:44+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.2.99" + }, + "plugin-api-version": "2.6.0" +} diff --git a/compiler/box/patches/ScoperAutoloaderGenerator.patch b/compiler/box/patches/ScoperAutoloaderGenerator.patch new file mode 100644 index 0000000000..5f993cc80e --- /dev/null +++ b/compiler/box/patches/ScoperAutoloaderGenerator.patch @@ -0,0 +1,20 @@ +--- src/Autoload/ScoperAutoloadGenerator.php 2025-06-04 20:36:51 ++++ src/Autoload/ScoperAutoloadGenerator.php 2025-06-04 20:37:31 +@@ -108,7 +108,7 @@ + \$loader = require_once __DIR__.'/autoload.php'; + // Ensure InstalledVersions is available + \$installedVersionsPath = __DIR__.'/composer/InstalledVersions.php'; +- if (file_exists(\$installedVersionsPath)) require_once \$installedVersionsPath; ++ if (!class_exists(\Composer\InstalledVersions::class, false) && file_exists(\$installedVersionsPath)) require_once \$installedVersionsPath; + + // Restore the backup and ensure the excluded files are properly marked as loaded + \$GLOBALS['__composer_autoload_files'] = \\array_merge( +@@ -140,7 +140,7 @@ + \$loader = require_once __DIR__.'/autoload.php'; + // Ensure InstalledVersions is available + \$installedVersionsPath = __DIR__.'/composer/InstalledVersions.php'; +- if (file_exists(\$installedVersionsPath)) require_once \$installedVersionsPath; ++ if (!class_exists(\Composer\InstalledVersions::class, false) && file_exists(\$installedVersionsPath)) require_once \$installedVersionsPath; + + // Restore the backup and ensure the excluded files are properly marked as loaded + \$GLOBALS['__composer_autoload_files'] = \\array_merge( diff --git a/compiler/build/box.json b/compiler/build/box.json index 9f0b7ee6c1..90bce8aff5 100644 --- a/compiler/build/box.json +++ b/compiler/build/box.json @@ -8,7 +8,8 @@ ], "files": [ "preload.php", - "vendor/composer/installed.php" + "vendor/composer/installed.php", + "vendor/attributes.php" ], "directories": [ "conf", diff --git a/compiler/build/box.phar b/compiler/build/box.phar deleted file mode 100644 index 76da031be9..0000000000 Binary files a/compiler/build/box.phar and /dev/null 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 f097836c95..98138a3bca 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -16,6 +16,12 @@ '../../stubs', '../../vendor/jetbrains/phpstorm-stubs', '../../vendor/phpstan/php-8-stubs/stubs', + '../../vendor/symfony/polyfill-php80', + '../../vendor/symfony/polyfill-php81', + '../../vendor/symfony/polyfill-php83', + '../../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; @@ -23,40 +29,36 @@ $stubs[] = $file->getPathName(); } -exec('git rev-parse --short HEAD', $gitCommitOutputLines, $gitExitCode); -if ($gitExitCode !== 0) { - die('Could not get Git commit'); +if ($_SERVER['PHAR_CHECKSUM'] ?? false) { + $prefix = '_PHPStan_checksum'; +} else { + exec('git rev-parse --short HEAD', $gitCommitOutputLines, $gitExitCode); + if ($gitExitCode !== 0) { + die('Could not get Git commit'); + } + + $prefix = sprintf('_PHPStan_%s', $gitCommitOutputLines[0]); } return [ - 'prefix' => sprintf('_PHPStan_%s', $gitCommitOutputLines[0]), + 'prefix' => $prefix, 'finders' => [], - 'files-whitelist' => $stubs, + 'exclude-files' => $stubs, + 'php-version' => '7.4', 'patchers' => [ - function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'bin/phpstan') { - return $content; - } - return str_replace('__DIR__ . \'/..', '\'phar://phpstan.phar', $content); - }, function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'vendor/nette/di/src/DI/Compiler.php') { return $content; } - return str_replace('|Nette\\\\DI\\\\Statement', sprintf('|\\\\%s\\\\Nette\\\\DI\\\\Statement', $prefix), $content); + 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( - sprintf('\'%s\\\\callable', $prefix), - '\'callable', - $content - ); - $content = str_replace( - '|Nette\\\\DI\\\\Definitions\\\\Statement', - sprintf('|%s\\\\Nette\\\\DI\\\\Definitions\\\\Statement', $prefix), + '|Nette\\DI\\Definitions\\Statement', + sprintf('|%s\\Nette\\DI\\Definitions\\Statement', $prefix), $content ); @@ -67,41 +69,20 @@ function (string $filePath, string $prefix, string $content): string { return $content; } $content = str_replace( - sprintf('\'%s\\\\string', $prefix), - '\'string', - $content - ); - $content = str_replace( - '|Nette\\\\DI\\\\Definitions\\\\Statement', - sprintf('|%s\\\\Nette\\\\DI\\\\Definitions\\\\Statement', $prefix), + '|Nette\\DI\\Definitions\\Statement', + sprintf('|%s\\Nette\\DI\\Definitions\\Statement', $prefix), $content ); return $content; }, - function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'src/Testing/PHPStanTestCase.php') { - return $content; - } - return str_replace(sprintf('\\%s\\PHPUnit\\Framework\\TestCase', $prefix), '\\PHPUnit\\Framework\\TestCase', $content); - }, - function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'src/Testing/LevelsTestCase.php') { - return $content; - } - return str_replace( - [sprintf('\\%s\\PHPUnit\\Framework\\AssertionFailedError', $prefix), sprintf('\\%s\\PHPUnit\\Framework\\TestCase', $prefix)], - ['\\PHPUnit\\Framework\\AssertionFailedError', '\\PHPUnit\\Framework\\TestCase'], - $content - ); - }, + function (string $filePath, string $prefix, string $content): string { if (strpos($filePath, 'src/') !== 0) { return $content; } - $content = str_replace(sprintf('\'%s\\\\r\\\\n\'', $prefix), '\'\\\\r\\\\n\'', $content); - $content = str_replace(sprintf('\'%s\\\\', $prefix), '\'', $content); + $content = str_replace(sprintf('\'%s\\r\\n\'', $prefix), '\'\\r\\n\'', $content); return $content; }, @@ -159,20 +140,29 @@ function (string $filePath, string $prefix, string $content): string { }, function (string $filePath, string $prefix, string $content): string { if (!in_array($filePath, [ + 'bin/phpstan', 'src/Testing/TestCaseSourceLocatorFactory.php', 'src/Testing/PHPStanTestCase.php', + 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php', ], 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; } - $content = str_replace('\'' . $prefix . '\\\\', '\'', $content); + $content = str_replace('\'' . $prefix . '\\', '\'', $content); return $content; }, @@ -181,33 +171,85 @@ function (string $filePath, string $prefix, string $content): string { return $content; } - $content = str_replace('\'' . $prefix . '\\\\', '\'', $content); + $content = str_replace('\'' . $prefix . '\\', '\'', $content); return $content; }, function (string $filePath, string $prefix, string $content): string { - if (!in_array($filePath, [ - 'src/Type/TypehintHelper.php', - 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionUnionType.php', - ], true)) { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + return $content; + } + + return str_replace('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\\ReflectionUnionType', $prefix), 'ReflectionUnionType', $content); + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); }, function (string $filePath, string $prefix, string $content): string { if (strpos($filePath, 'src/') !== 0) { return $content; } - return str_replace(sprintf('%s\\Attribute', $prefix), 'Attribute', $content); - } + return str_replace([ + sprintf('\'%s\\BcMath\\', $prefix), + sprintf('\'%s\\Dom\\', $prefix), + sprintf('\'%s\\FFI\\', $prefix), + sprintf('\'%s\\Ds\\', $prefix), + ], [ + '\'BcMath\\', + '\'Dom\\', + '\'FFI\\', + '\'Ds\\', + ], $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (strpos($filePath, 'src/Testing/ErrorFormatterTestCase.php') !== 0) { + return $content; + } + + return str_replace(sprintf('new Error(\'%s\\Foobar\\Buz', $prefix), 'new Error(\'Foobar\\Buz', $content); + }, ], - 'whitelist' => [ - 'PHPStan\*', - 'PhpParser\*', - 'Hoa\*', + 'exclude-namespaces' => [ + 'PHPStan', + 'PHPUnit', + 'PhpParser', + 'Hoa', + 'Symfony\Polyfill\Php80', + 'Symfony\Polyfill\Php81', + 'Symfony\Polyfill\Php83', + 'Symfony\Polyfill\Mbstring', + 'Symfony\Polyfill\Intl\Normalizer', + 'Symfony\Polyfill\Intl\Grapheme', ], - 'whitelist-global-functions' => false, - 'whitelist-global-classes' => false, + 'expose-global-functions' => false, + 'expose-global-classes' => false, ]; diff --git a/compiler/composer.json b/compiler/composer.json index 109608810b..d69b1a1a8f 100644 --- a/compiler/composer.json +++ b/compiler/composer.json @@ -4,12 +4,14 @@ "description": "PHAR Compiler for PHPStan", "license": ["MIT"], "require": { - "php": "^7.3", + "php": "^8.2", "nette/neon": "^3.0.0", - "symfony/console": "^5.2.2", - "symfony/process": "^5.2.2", - "symfony/filesystem": "^5.2.2", - "symfony/finder": "^5.2.2" + "ondrejmirtes/simple-downgrader": "^2.2.2", + "seld/phar-utils": "^1.2", + "symfony/console": "^5.4.43", + "symfony/filesystem": "^5.4.43", + "symfony/finder": "^5.4.43", + "symfony/process": "^5.4.43" }, "autoload": { "psr-4": { @@ -22,12 +24,13 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5.1", - "phpstan/phpstan-phpunit": "^1.0" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.1" }, "config": { "platform": { - "php": "7.3.24" + "php": "8.2.99" }, "platform-check": false, "sort-packages": true diff --git a/compiler/composer.lock b/compiler/composer.lock index 4517aaff62..cecb355cfc 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -4,35 +4,38 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5e13c6da184e97394b42dff8a20ba685", + "content-hash": "421cae13caa4a836476ab23b026961de", "packages": [ { "name": "nette/neon", - "version": "v3.2.2", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "e4ca6f4669121ca6876b1d048c612480e39a28d5" + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/e4ca6f4669121ca6876b1d048c612480e39a28d5", - "reference": "e4ca6f4669121ca6876b1d048c612480e39a28d5", + "url": "https://api.github.com/repos/nette/neon/zipball/3411aa86b104e2d5b7e760da4600865ead963c3c", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": "8.0 - 8.4" }, "require-dev": { - "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.7" }, + "bin": [ + "bin/neon-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -67,28 +70,274 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.2.2" + "source": "https://github.com/nette/neon/tree/v3.4.4" + }, + "time": "2024-10-04T22:00:08+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.6.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "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.6.0" + }, + "time": "2025-07-27T20:03:57+00:00" + }, + { + "name": "ondrejmirtes/simple-downgrader", + "version": "2.2.2", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/simple-downgrader.git", + "reference": "5de54eaa47dac5142e36b4085a12e5103e93366c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/5de54eaa47dac5142e36b4085a12e5103e93366c", + "reference": "5de54eaa47dac5142e36b4085a12e5103e93366c", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.5", + "nikic/php-parser": "^5.5.0", + "php": "^8.2", + "phpstan/phpdoc-parser": "^2.0", + "symfony/console": "^5.4.47", + "symfony/finder": "^5.4.45", + "symfony/polyfill-php80": "^1.32" + }, + "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/" + ] + } }, - "time": "2021-02-28T12:30:32+00:00" + "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.2.2" + }, + "time": "2025-09-20T18:37:56+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "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.2.0" + }, + "time": "2025-07-13T07:04:09+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -115,34 +364,83 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-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": "2021-03-05T17:36:06+00:00" + "time": "2022-08-31T10:31:18+00:00" }, { "name": "symfony/console", - "version": "v5.3.2", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1", - "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2", - "symfony/string": "^5.1" + "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", @@ -150,16 +448,16 @@ "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "psr/log": "^1|^2", + "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", @@ -194,12 +492,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.2" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -215,33 +513,33 @@ "type": "tidelift" } ], - "time": "2021-06-12T09:42:48+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.4.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.0-dev" } }, "autoload": { @@ -266,7 +564,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" }, "funding": [ { @@ -282,25 +580,30 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2022-01-02T09:55:41+00:00" }, { "name": "symfony/filesystem", - "version": "v5.3.0", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "348116319d7fb7d1faa781d26a48922428013eb2" + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/348116319d7fb7d1faa781d26a48922428013eb2", - "reference": "348116319d7fb7d1faa781d26a48922428013eb2", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8" + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4" }, "type": "library", "autoload": { @@ -328,7 +631,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.3.0" + "source": "https://github.com/symfony/filesystem/tree/v5.4.45" }, "funding": [ { @@ -344,24 +647,26 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2024-10-22T13:05:35+00:00" }, { "name": "symfony/finder", - "version": "v5.3.0", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6", - "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -389,7 +694,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.0" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -405,45 +710,45 @@ "type": "tidelift" } ], - "time": "2021-05-26T12:52:38+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -468,7 +773,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -484,45 +789,42 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/24b72c6baa32c746a4d0840147c9715e42bb68ab", - "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -549,7 +851,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -565,45 +867,42 @@ "type": "tidelift" } ], - "time": "2021-05-27T09:17:38+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -633,7 +932,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -649,45 +948,46 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -713,7 +1013,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -729,44 +1029,41 @@ "type": "tidelift" } ], - "time": "2021-05-27T09:27:20+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -792,7 +1089,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { @@ -808,42 +1105,39 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.23.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0", - "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -875,7 +1169,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -891,25 +1185,25 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/process", - "version": "v5.3.2", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "714b47f9196de61a196d86c4bad5f09201b307df" + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/714b47f9196de61a196d86c4bad5f09201b307df", - "reference": "714b47f9196de61a196d86c4bad5f09201b307df", + "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d", + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -937,7 +1231,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.3.2" + "source": "https://github.com/symfony/process/tree/v5.4.47" }, "funding": [ { @@ -953,37 +1247,40 @@ "type": "tidelift" } ], - "time": "2021-06-12T10:15:01+00:00" + "time": "2024-11-06T11:36:42+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.4.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1" + "php": ">=8.0.2", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.0-dev" } }, "autoload": { @@ -1016,7 +1313,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" }, "funding": [ { @@ -1032,44 +1329,46 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2022-05-30T19:17:58+00:00" }, { "name": "symfony/string", - "version": "v5.3.2", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "0732e97e41c0a590f77e231afc16a327375d50b0" + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/0732e97e41c0a590f77e231afc16a327375d50b0", - "reference": "0732e97e41c0a590f77e231afc16a327375d50b0", + "url": "https://api.github.com/repos/symfony/string/zipball/d9e72497367c23e08bf94176d2be45b00a9d232a", + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -1099,7 +1398,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.2" + "source": "https://github.com/symfony/string/tree/v6.0.19" }, "funding": [ { @@ -1110,335 +1409,46 @@ "url": "https://github.com/fabpot", "type": "github" }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-06-06T09:51:56+00:00" - } - ], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^8.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "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.4.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": "2020-11-10T18:47:58+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.10.2", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "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.10.2" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2020-11-13T09:40:50+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.10.5", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", - "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.10.5" - }, - "time": "2021-05-03T19:11:20+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "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/master" - }, - "time": "2020-06-27T14:33:11+00:00" - }, - { - "name": "phar-io/version", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", - "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" + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" - }, - "time": "2021-02-23T14:00:09+00:00" - }, + "time": "2023-01-01T08:36:10+00:00" + } + ], + "packages-dev": [ { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "doctrine/instantiator", + "version": "1.5.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } + "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": { - "phpDocumentor\\Reflection\\": "src/" + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1447,228 +1457,240 @@ ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "constructor", + "instantiate" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, - "time": "2020-06-27T09:03:43+00:00" + "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": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "name": "myclabs/deep-copy", + "version": "1.13.4", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "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", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, - "time": "2020-09-03T19:13:55+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "name": "phar-io/manifest", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" + "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-1.x": "1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "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": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2020-09-17T18:55:26+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { - "name": "phpspec/prophecy", - "version": "1.13.0", + "name": "phar-io/version", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" }, { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], + "description": "Library for handling version information and constraints", "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpstan/phpstan", - "version": "dev-master", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "ad96e5efd921513113c96245e0e3ed369c5544d4" + "reference": "578fa296a166605d97b94091f724f1257185d278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad96e5efd921513113c96245e0e3ed369c5544d4", - "reference": "ad96e5efd921513113c96245e0e3ed369c5544d4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", + "reference": "578fa296a166605d97b94091f724f1257185d278", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" }, - "default-branch": true, "bin": [ "phpstan", "phpstan.phar" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "files": [ "bootstrap.php" @@ -1679,9 +1701,16 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/master" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -1691,50 +1720,40 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2021-09-12T20:30:21+00:00" + "time": "2025-09-19T08:58:49+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "dev-master", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "6c0e48e98f082e94be11bca4db64489194c66b06" + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6c0e48e98f082e94be11bca4db64489194c66b06", - "reference": "6c0e48e98f082e94be11bca4db64489194c66b06", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc", + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.18" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", @@ -1754,50 +1773,50 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/master" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7" }, - "time": "2021-09-12T20:25:39+00:00" + "time": "2025-07-13T11:31:46+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -1825,7 +1844,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -1833,20 +1853,20 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { @@ -1885,7 +1905,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" }, "funding": [ { @@ -1893,7 +1913,7 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2021-12-02T12:48:52+00:00" }, { "name": "phpunit/php-invoker", @@ -2078,55 +2098,50 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.6", + "version": "9.6.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb" + "reference": "a8017241a554a259997a5285eee5d10c69ff7187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb", - "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8017241a554a259997a5285eee5d10c69ff7187", + "reference": "a8017241a554a259997a5285eee5d10c69ff7187", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.7", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -2134,15 +2149,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2165,32 +2180,45 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.6" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.28" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "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": "2021-06-23T05:14:38+00:00" + "time": "2025-09-23T06:20:12+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -2225,7 +2253,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -2233,7 +2261,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -2348,16 +2376,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -2410,32 +2438,44 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "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/sebastian/comparator", + "type": "tidelift" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2467,7 +2507,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -2475,20 +2515,20 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -2533,7 +2573,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -2541,20 +2581,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -2596,7 +2636,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -2604,20 +2644,20 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "eb49b981ef0817890129cb70f774506bebe57740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/eb49b981ef0817890129cb70f774506bebe57740", + "reference": "eb49b981ef0817890129cb70f774506bebe57740", "shasum": "" }, "require": { @@ -2666,35 +2706,47 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.7" }, "funding": [ { "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/sebastian/exporter", + "type": "tidelift" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2025-09-22T05:18:21+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.3", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -2737,32 +2789,44 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "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/sebastian/global-state", + "type": "tidelift" } ], - "time": "2021-06-11T13:31:12+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2794,7 +2858,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -2802,7 +2866,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -2918,16 +2982,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -2966,10 +3030,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2977,20 +3041,20 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -3002,7 +3066,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -3023,8 +3087,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -3032,32 +3095,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3080,7 +3143,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -3088,7 +3151,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -3145,16 +3208,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.0", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -3183,7 +3246,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -3191,78 +3254,20 @@ "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.3" + "php": "^8.2" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { - "php": "7.3.24" + "php": "8.2.99" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.6.0" } diff --git a/compiler/patches/stubs/PDO/PDO.stub.patch b/compiler/patches/stubs/PDO/PDO.stub.patch deleted file mode 100644 index 0f65cc6010..0000000000 --- a/compiler/patches/stubs/PDO/PDO.stub.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- PDO/PDO.stub 2020-06-14 14:26:12.000000000 +0200 -+++ PDO/PDO2.stub 2020-06-14 14:26:12.000000000 +0200 -@@ -843,6 +843,15 @@ - */ - const SQLITE_ATTR_EXTENDED_RESULT_CODES = 2; - -+ const FB_ATTR_DATE_FORMAT = 1; -+ const FB_ATTR_TIME_FORMAT = 2; -+ const FB_ATTR_TIMESTAMP_FORMAT = 3; -+ -+ const OCI_ATTR_ACTION = 1; -+ const OCI_ATTR_CLIENT_INFO = 2; -+ const OCI_ATTR_CLIENT_IDENTIFIER = 3; -+ const OCI_ATTR_MODULE = 4; -+ - /** - * (PHP 5 >= 5.1.0, PHP 7, PECL pdo >= 0.1.0)
- * Creates a PDO instance representing a connection to a database diff --git a/compiler/src/Console/CompileCommand.php b/compiler/src/Console/CompileCommand.php deleted file mode 100644 index 194198f2f2..0000000000 --- a/compiler/src/Console/CompileCommand.php +++ /dev/null @@ -1,246 +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->fixComposerJson($this->buildDir); - $this->renamePhpStormStubs(); - $this->patchPhpStormStubs($output); - $this->renamePhp8Stubs(); - $this->transformSource(); - - $this->processFactory->create(['php', 'box.phar', 'compile', '--no-parallel'], $this->dataDir); - - return 0; - } - - private function fixComposerJson(string $buildDir): void - { - $json = json_decode($this->filesystem->read($buildDir . '/composer.json'), true); - - unset($json['replace']); - $json['name'] = 'phpstan/phpstan'; - $json['require']['php'] = '^7.1'; - - // simplify autoload (remove not packed build directory] - $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; - - $encodedJson = json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); - if ($encodedJson === false) { - throw new \Exception('json_encode() was not successful.'); - } - - $this->filesystem->write($buildDir . '/composer.json', $encodedJson); - } - - private function renamePhpStormStubs(): void - { - $directory = $this->buildDir . '/vendor/jetbrains/phpstorm-stubs'; - if (!is_dir($directory)) { - return; - } - - $stubFinder = \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($stubsMapPath, $stubsMapContents); - if ($putSuccess === false) { - throw new \PHPStan\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 = \Symfony\Component\Finder\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 \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($stubsMapPath, $stubsMapContents); - if ($putSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); - } - } - - private function patchPhpStormStubs(OutputInterface $output): void - { - $stubFinder = \Symfony\Component\Finder\Finder::create(); - $stubsDirectory = __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs'; - foreach ($stubFinder->files()->name('*.patch')->in(__DIR__ . '/../../patches/stubs') as $patchFile) { - $absolutePatchPath = $patchFile->getPathname(); - $patchPath = $patchFile->getRelativePathname(); - $stubPath = realpath($stubsDirectory . '/' . dirname($patchPath) . '/' . basename($patchPath, '.patch')); - if ($stubPath === false) { - $output->writeln(sprintf('Stub %s not found.', $stubPath)); - continue; - } - $this->patchFile($output, $stubPath, $absolutePatchPath); - } - } - - private function buildPreloadScript(): void - { - $vendorDir = $this->buildDir . '/vendor'; - if (!is_dir($vendorDir . '/nikic/php-parser/lib/PhpParser')) { - return; - } - - $preloadScript = $this->buildDir . '/preload.php'; - $template = <<<'php' -files()->name('*.php')->in([ - $this->buildDir . '/src', - $vendorDir . '/nikic/php-parser/lib/PhpParser', - $vendorDir . '/phpstan/phpdoc-parser/src', - ])->exclude([ - 'Testing', - ]) as $phpFile) { - $realPath = $phpFile->getRealPath(); - if ($realPath === false) { - return; - } - $path = substr($realPath, strlen($root)); - $output .= 'require_once __DIR__ . ' . var_export($path, true) . ';' . "\n"; - } - - file_put_contents($preloadScript, sprintf($template, $output)); - } - - private function deleteUnnecessaryVendorCode(): void - { - $vendorDir = $this->buildDir . '/vendor'; - if (!is_dir($vendorDir . '/nikic/php-parser')) { - return; - } - - @unlink($vendorDir . '/nikic/php-parser/grammar/rebuildParsers.php'); - @unlink($vendorDir . '/nikic/php-parser/bin/php-parse'); - } - - private function 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..cc8ddbac5d --- /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.4', $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 72e1a5d5dc..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->method('setOutput'); - $processFactory->method('create')->with(['php', 'box.phar', 'compile', '--no-parallel'], 'foo')->willReturn($process); - - $application = new Application(); - $application->add(new CompileCommand($filesystem, $processFactory, 'foo', 'bar')); - - $command = $application->find('phpstan:compile'); - $commandTester = new CommandTester($command); - $exitCode = $commandTester->execute([ - 'command' => $command->getName(), - ]); - - self::assertSame(0, $exitCode); - } - -} diff --git a/compiler/tests/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 88aaae4948..bb18a3f101 100644 --- a/composer.json +++ b/composer.json @@ -5,65 +5,134 @@ "MIT" ], "require": { - "php": "^7.4 || ^8.0", - "clue/block-react": "^1.4", + "php": "^8.2", + "composer-runtime-api": "^2.0", "clue/ndjson-react": "^1.0", "composer/ca-bundle": "^1.2", - "composer/xdebug-handler": "^2.0.1", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "fidry/cpu-core-counter": "^1.2", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", - "hoa/regex": "1.17.01.13", - "jean85/pretty-package-versions": "^1.0.3", - "jetbrains/phpstorm-stubs": "dev-master#82595d7a426c4b3d1e3a7d604ad3f99534784599", + "hoa/file": "1.17.07.11", + "jetbrains/phpstorm-stubs": "dev-master#d1ee5e570343bd4276a3d5959e6e1c2530b006d0", "nette/bootstrap": "^3.0", - "nette/di": "^3.0.5", - "nette/finder": "^2.5", - "nette/neon": "^3.0", - "nette/schema": "^1.0", - "nette/utils": "^3.1.3", - "nikic/php-parser": "4.13.0", + "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.6.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "4.3.70", - "phpstan/php-8-stubs": "^0.1.23", - "phpstan/phpdoc-parser": "^1.2.0", - "react/child-process": "^0.6.1", - "react/event-loop": "^1.1", + "ondrejmirtes/better-reflection": "6.64.0.1", + "ondrejmirtes/composer-attribute-collector": "^1.1.1", + "ondrejmirtes/php-merge": "^4.1", + "phpstan/php-8-stubs": "0.4.32", + "phpstan/phpdoc-parser": "2.3.0", + "psr/http-message": "^1.1", + "react/async": "^3", + "react/child-process": "^0.7", + "react/dns": "^1.10", + "react/event-loop": "^1.2", "react/http": "^1.1", - "react/promise": "^2.8", + "react/promise": "^3.2", "react/socket": "^1.3", "react/stream": "^1.1", - "symfony/console": "^4.3", - "symfony/finder": "^4.3", - "symfony/service-contracts": "1.1.8" + "sebastian/diff": "^6.0.2", + "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/polyfill-php83": "^1.33", + "symfony/process": "^5.4.3", + "symfony/service-contracts": "^2.5.0", + "symfony/string": "^5.4.3" }, "replace": { - "phpstan/phpstan": "self.version" + "phpstan/phpstan": "2.1.x", + "symfony/polyfill-php73": "*" }, "require-dev": { - "brianium/paratest": "^6.2.0", - "nategood/httpful": "^0.2.20", + "cweagans/composer-patches": "^1.7.3", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-nette": "^1.0", - "phpstan/phpstan-php-parser": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5.4", - "vaimo/composer-patches": "^4.22" + "phpstan/phpstan-deprecation-rules": "^2.0.2", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.23", + "shipmonk/composer-dependency-analyser": "^1.5", + "shipmonk/dead-code-detector": "^0.12.0", + "shipmonk/name-collision-detector": "^2.0" }, "config": { "platform": { - "php": "7.4.6" + "php": "8.2.99" }, "platform-check": false, - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "cweagans/composer-patches": true, + "ondrejmirtes/composer-attribute-collector": true, + "vaimo/composer-patches": true + } }, "extra": { - "branch-alias": { - "dev-master": "1.0-dev" + "composer-attribute-collector": { + "include": [ + "src" + ] }, - "patcher": { - "search": "patches" + "composer-exit-on-patch-failure": true, + "patches": { + "composer/ca-bundle": [ + "patches/cloudflare-ca.patch" + ], + "hoa/exception": [ + "patches/Idle.patch" + ], + "hoa/file": [ + "patches/File.patch", + "patches/Read.patch" + ], + "hoa/iterator": [ + "patches/Buffer.patch", + "patches/Lookahead.patch" + ], + "hoa/compiler": [ + "patches/HoaException.patch", + "patches/HoaParser.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", + "patches/Resolver.patch" + ], + "symfony/console": [ + "patches/OutputFormatter.patch" + ] } }, "autoload": { @@ -72,7 +141,7 @@ "src/" ] }, - "files": ["src/dumpType.php","src/Testing/functions.php"] + "files": ["src/debugScope.php", "src/dumpType.php", "src/autoloadFunctions.php", "src/Testing/functions.php"] }, "autoload-dev": { "psr-4": { @@ -85,6 +154,83 @@ "tests/PHPStan" ] }, + "repositories": [ + { + "type": "package", + "package": { + "name": "nette/di", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "support": { + "issues": "https://github.com/nette/di/issues", + "source": "https://github.com/nette/di/tree/v3.1.5" + }, + "time": "2023-10-02T19:58:38+00:00" + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "bin": [ diff --git a/composer.lock b/composer.lock index 5e67ba1ccf..1704fe88e8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,37 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c1d91467e312e364a2d0be0d50163a50", + "content-hash": "31c7850e5e9191189f53f00131384a72", "packages": [ { - "name": "clue/block-react", - "version": "v1.4.0", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/clue/reactphp-block.git", - "reference": "c8e7583ae55127b89d6915480ce295bac81c4f88" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-block/zipball/c8e7583ae55127b89d6915480ce295bac81c4f88", - "reference": "c8e7583ae55127b89d6915480ce295bac81c4f88", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/promise": "^2.7 || ^1.2.1", - "react/promise-timer": "^1.5" + "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/http": "^1.0" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", "autoload": { - "files": [ - "src/functions_include.php" - ] + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -46,21 +44,19 @@ "email": "christian@clue.engineering" } ], - "description": "Lightweight library that eases integrating async components built for ReactPHP in a traditional, blocking environment.", - "homepage": "https://github.com/clue/reactphp-block", + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", "keywords": [ - "async", - "await", - "blocking", - "event loop", - "promise", + "NDJSON", + "json", + "jsonlines", + "newline", "reactphp", - "sleep", - "synchronous" + "streaming" ], "support": { - "issues": "https://github.com/clue/reactphp-block/issues", - "source": "https://github.com/clue/reactphp-block/tree/v1.4.0" + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, "funding": [ { @@ -72,34 +68,42 @@ "type": "github" } ], - "time": "2020-08-21T14:09:44+00:00" + "time": "2022-12-23T10:58:28+00:00" }, { - "name": "clue/ndjson-react", - "version": "v1.1.0", + "name": "composer/ca-bundle", + "version": "1.5.8", "source": { "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "767ec9543945802b5766fab0da4520bf20626f66" + "url": "https://github.com/composer/ca-bundle.git", + "reference": "719026bb30813accb68271fee7e39552a58e9f65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/767ec9543945802b5766fab0da4520bf20626f66", - "reference": "767ec9543945802b5766fab0da4520bf20626f66", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65", + "reference": "719026bb30813accb68271fee7e39552a58e9f65", "shasum": "" }, "require": { - "php": ">=5.3", - "react/stream": "^1.0 || ^0.7 || ^0.6" + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.0 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + "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": { - "Clue\\React\\NDJson\\": "src/" + "Composer\\CaBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -108,59 +112,75 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" ], "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.1.0" + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.8" }, - "time": "2020-02-04T11:48:52+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T18:49:47+00:00" }, { - "name": "composer/ca-bundle", - "version": "1.2.8", + "name": "composer/pcre", + "version": "3.3.1", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "8a7ecad675253e4654ea05505233285377405215" + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", - "reference": "8a7ecad675253e4654ea05505233285377405215", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", - "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { "psr-4": { - "Composer\\CaBundle\\": "src" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -174,18 +194,16 @@ "homepage": "http://seld.be" } ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.8" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -201,44 +219,38 @@ "type": "tidelift" } ], - "time": "2020-08-23T12:54:47+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { - "name": "composer/package-versions-deprecated", - "version": "1.11.99", + "name": "composer/semver", + "version": "3.4.4", "source": { "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855" + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" - }, - "replace": { - "ocramius/package-versions": "1.11.99" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PackageVersions\\Installer", "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "PackageVersions\\": "src/PackageVersions" + "Composer\\Semver\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -247,18 +259,32 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" }, { "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/master" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -268,35 +294,33 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2020-08-25T05:50:16+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.1", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -320,9 +344,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -338,32 +362,32 @@ "type": "tidelift" } ], - "time": "2021-05-05T19:37:51+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -383,9 +407,126 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2020-11-24T22:02:12+00:00" }, { "name": "hoa/compiler", @@ -475,6 +616,7 @@ "issues": "https://github.com/hoaproject/Compiler/issues", "source": "https://central.hoa-project.net/Resource/Library/Compiler" }, + "abandoned": true, "time": "2017-08-08T07:44:07+00:00" }, { @@ -506,12 +648,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, "files": [ "Prelude.php" - ] + ], + "psr-4": { + "Hoa\\Consistency\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -546,6 +688,7 @@ "issues": "https://github.com/hoaproject/Consistency/issues", "source": "https://central.hoa-project.net/Resource/Library/Consistency" }, + "abandoned": true, "time": "2017-05-02T12:18:12+00:00" }, { @@ -610,6 +753,7 @@ "issues": "https://github.com/hoaproject/Event/issues", "source": "https://central.hoa-project.net/Resource/Library/Event" }, + "abandoned": true, "time": "2017-01-13T15:30:50+00:00" }, { @@ -672,6 +816,7 @@ "issues": "https://github.com/hoaproject/Exception/issues", "source": "https://central.hoa-project.net/Resource/Library/Exception" }, + "abandoned": true, "time": "2017-01-16T07:53:27+00:00" }, { @@ -742,6 +887,7 @@ "issues": "https://github.com/hoaproject/File/issues", "source": "https://central.hoa-project.net/Resource/Library/File" }, + "abandoned": true, "time": "2017-07-11T07:42:15+00:00" }, { @@ -804,6 +950,7 @@ "issues": "https://github.com/hoaproject/Iterator/issues", "source": "https://central.hoa-project.net/Resource/Library/Iterator" }, + "abandoned": true, "time": "2017-01-10T10:34:47+00:00" }, { @@ -877,6 +1024,7 @@ "issues": "https://github.com/hoaproject/Math/issues", "source": "https://central.hoa-project.net/Resource/Library/Math" }, + "abandoned": true, "time": "2017-05-16T08:02:17+00:00" }, { @@ -907,12 +1055,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, "files": [ "Wrapper.php" - ] + ], + "psr-4": { + "Hoa\\Protocol\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -945,6 +1093,7 @@ "issues": "https://github.com/hoaproject/Protocol/issues", "source": "https://central.hoa-project.net/Resource/Library/Protocol" }, + "abandoned": true, "time": "2017-01-14T12:26:10+00:00" }, { @@ -1009,6 +1158,7 @@ "issues": "https://github.com/hoaproject/Regex/issues", "source": "https://central.hoa-project.net/Resource/Library/Regex" }, + "abandoned": true, "time": "2017-01-13T16:10:24+00:00" }, { @@ -1081,6 +1231,7 @@ "issues": "https://github.com/hoaproject/Stream/issues", "source": "https://central.hoa-project.net/Resource/Library/Stream" }, + "abandoned": true, "time": "2017-02-21T16:01:06+00:00" }, { @@ -1149,6 +1300,7 @@ "issues": "https://github.com/hoaproject/Ustring/issues", "source": "https://central.hoa-project.net/Resource/Library/Ustring" }, + "abandoned": true, "time": "2017-01-16T07:08:25+00:00" }, { @@ -1212,6 +1364,7 @@ "issues": "https://github.com/hoaproject/Visitor/issues", "source": "https://central.hoa-project.net/Resource/Library/Visitor" }, + "abandoned": true, "time": "2017-01-16T07:02:03+00:00" }, { @@ -1272,83 +1425,28 @@ "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": "jean85/pretty-package-versions", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "a917488320c20057da87f67d0d40543dd9427f7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/a917488320c20057da87f67d0d40543dd9427f7a", - "reference": "a917488320c20057da87f67d0d40543dd9427f7a", - "shasum": "" - }, - "require": { - "composer/package-versions-deprecated": "^1.8.0", - "php": "^7.0|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0|^8.5|^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A wrapper for ocramius/package-versions to get pretty versions strings", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/1.5.1" - }, - "time": "2020-09-14T08:43:34+00:00" - }, { "name": "jetbrains/phpstorm-stubs", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "82595d7a426c4b3d1e3a7d604ad3f99534784599" + "reference": "d1ee5e570343bd4276a3d5959e6e1c2530b006d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/82595d7a426c4b3d1e3a7d604ad3f99534784599", - "reference": "82595d7a426c4b3d1e3a7d604ad3f99534784599", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/d1ee5e570343bd4276a3d5959e6e1c2530b006d0", + "reference": "d1ee5e570343bd4276a3d5959e6e1c2530b006d0", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "@stable", - "nikic/php-parser": "@stable", - "php": "^8.0", - "phpdocumentor/reflection-docblock": "@stable", - "phpunit/phpunit": "@stable" + "friendsofphp/php-cs-fixer": "^v3.86", + "nikic/php-parser": "^v5.6", + "phpdocumentor/reflection-docblock": "^5.6", + "phpunit/phpunit": "^12.3" }, "default-branch": true, "type": "library", @@ -1376,33 +1474,33 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2021-09-09T16:57:58+00:00" + "time": "2025-09-20T07:44:45+00:00" }, { "name": "nette/bootstrap", - "version": "v3.0.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/nette/bootstrap.git", - "reference": "67830a65b42abfb906f8e371512d336ebfb5da93" + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/bootstrap/zipball/67830a65b42abfb906f8e371512d336ebfb5da93", - "reference": "67830a65b42abfb906f8e371512d336ebfb5da93", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/1a7965b4ee401ad0e3f673b9c016d2481afdc280", + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280", "shasum": "" }, "require": { - "nette/di": "^3.0", - "nette/utils": "^3.0", - "php": ">=7.1" + "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.2", - "nette/application": "^3.0", + "latte/latte": "^2.8", + "nette/application": "^3.1", "nette/caching": "^3.0", "nette/database": "^3.0", "nette/forms": "^3.0", @@ -1422,7 +1520,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1446,7 +1544,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", "homepage": "https://nette.org", "keywords": [ "bootstrapping", @@ -1455,45 +1553,42 @@ ], "support": { "issues": "https://github.com/nette/bootstrap/issues", - "source": "https://github.com/nette/bootstrap/tree/master" + "source": "https://github.com/nette/bootstrap/tree/v3.1.4" }, - "time": "2020-05-26T08:46:23+00:00" + "time": "2022-12-14T15:23:02+00:00" }, { "name": "nette/di", - "version": "v3.0.5", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "766e8185196a97ded4f9128db6d79a3a124b7eb6" + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/766e8185196a97ded4f9128db6d79a3a124b7eb6", - "reference": "766e8185196a97ded4f9128db6d79a3a124b7eb6", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", "shasum": "" }, "require": { "ext-tokenizer": "*", - "nette/neon": "^3.0", - "nette/php-generator": "^3.3.3", - "nette/robot-loader": "^3.2", - "nette/schema": "^1.0", - "nette/utils": "^3.1", - "php": ">=7.1" - }, - "conflict": { - "nette/bootstrap": "<3.0" + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" }, "require-dev": { - "nette/tester": "^2.2", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1517,7 +1612,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", "homepage": "https://nette.org", "keywords": [ "compiled", @@ -1530,22 +1625,22 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/master" + "source": "https://github.com/nette/di/tree/v3.1.5" }, - "time": "2020-08-13T13:04:23+00:00" + "time": "2023-10-02T19:58:38+00:00" }, { "name": "nette/finder", - "version": "v2.5.2", + "version": "v2.6.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50" + "reference": "991aefb42860abeab8e003970c3809a9d83cb932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/4ad2c298eb8c687dd0e74ae84206a4186eeaed50", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50", + "url": "https://api.github.com/repos/nette/finder/zipball/991aefb42860abeab8e003970c3809a9d83cb932", + "reference": "991aefb42860abeab8e003970c3809a9d83cb932", "shasum": "" }, "require": { @@ -1563,7 +1658,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -1574,9 +1669,9 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" - ], + "GPL-2.0-only", + "GPL-3.0-only" + ], "authors": [ { "name": "David Grudl", @@ -1597,38 +1692,40 @@ ], "support": { "issues": "https://github.com/nette/finder/issues", - "source": "https://github.com/nette/finder/tree/v2.5.2" + "source": "https://github.com/nette/finder/tree/v2.6.0" }, - "time": "2020-01-03T20:35:40+00:00" + "time": "2022-10-13T01:31:15+00:00" }, { "name": "nette/neon", - "version": "v3.2.1", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "a5b3a60833d2ef55283a82d0c30b45d136b29e75" + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/a5b3a60833d2ef55283a82d0c30b45d136b29e75", - "reference": "a5b3a60833d2ef55283a82d0c30b45d136b29e75", + "url": "https://api.github.com/repos/nette/neon/zipball/bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", "shasum": "" }, "require": { - "ext-iconv": "*", "ext-json": "*", - "php": ">=7.1" + "php": "7.1 - 8.4" }, "require-dev": { "nette/tester": "^2.0", "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.7" }, + "bin": [ + "bin/neon-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1663,33 +1760,33 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/master" + "source": "https://github.com/nette/neon/tree/v3.3.4" }, - "time": "2020-07-31T12:28:05+00:00" + "time": "2024-10-04T22:17:24+00:00" }, { "name": "nette/php-generator", - "version": "v3.5.0", + "version": "v3.6.9", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "9162f7455059755dcbece1b5570d1bbfc6f0ab0d" + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/9162f7455059755dcbece1b5570d1bbfc6f0ab0d", - "reference": "9162f7455059755dcbece1b5570d1bbfc6f0ab0d", + "url": "https://api.github.com/repos/nette/php-generator/zipball/d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", "shasum": "" }, "require": { "nette/utils": "^3.1.2", - "php": ">=7.1" + "php": ">=7.2 <8.3" }, "require-dev": { - "nette/tester": "^2.0", - "nikic/php-parser": "^4.4", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.13", "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.8" }, "suggest": { "nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()" @@ -1697,7 +1794,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.5-dev" + "dev-master": "3.6-dev" } }, "autoload": { @@ -1721,7 +1818,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.1 features.", "homepage": "https://nette.org", "keywords": [ "code", @@ -1731,22 +1828,22 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v3.5.0" + "source": "https://github.com/nette/php-generator/tree/v3.6.9" }, - "time": "2020-11-02T16:16:58+00:00" + "time": "2022-10-04T11:49:47+00:00" }, { "name": "nette/robot-loader", - "version": "v3.3.1", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b" + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b", - "reference": "15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/e2adc334cb958164c050f485d99c44c430f51fe2", + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2", "shasum": "" }, "require": { @@ -1763,7 +1860,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1798,36 +1895,38 @@ ], "support": { "issues": "https://github.com/nette/robot-loader/issues", - "source": "https://github.com/nette/robot-loader/tree/v3.3.1" + "source": "https://github.com/nette/robot-loader/tree/v3.4.1" }, - "time": "2020-09-15T15:14:17+00:00" + "time": "2021-08-25T15:53:54+00:00" }, { "name": "nette/schema", - "version": "v1.0.2", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4" + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/febf71fb4052c824046f5a33f4f769a6e7fa0cb4", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", "shasum": "" }, "require": { - "nette/utils": "^3.1", - "php": ">=7.1" + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": "7.1 - 8.3" }, "require-dev": { - "nette/tester": "^2.2", - "phpstan/phpstan-nette": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.7" }, "type": "library", "extra": { - "branch-alias": [] + "branch-alias": { + "dev-master": "1.2-dev" + } }, "autoload": { "classmap": [ @@ -1837,8 +1936,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { @@ -1858,30 +1957,34 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.0.2" + "source": "https://github.com/nette/schema/tree/v1.2.5" }, - "time": "2020-01-06T22:52:48+00:00" + "time": "2023-10-05T20:37:59+00:00" }, { "name": "nette/utils", - "version": "v3.1.3", + "version": "v3.2.10", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c09937fbb24987b2a41c6022ebe84f4f1b8eec0f" + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c09937fbb24987b2a41c6022ebe84f4f1b8eec0f", - "reference": "c09937fbb24987b2a41c6022ebe84f4f1b8eec0f", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2 <8.4" + }, + "conflict": { + "nette/di": "<3.0.6" }, "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", "nette/tester": "~2.0", - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.3" }, "suggest": { @@ -1896,7 +1999,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -1920,7 +2023,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", "homepage": "https://nette.org", "keywords": [ "array", @@ -1940,31 +2043,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.1.3" + "source": "https://github.com/nette/utils/tree/v3.2.10" }, - "time": "2020-08-07T10:34:21+00:00" + "time": "2023-07-30T15:38:18+00:00" }, { "name": "nikic/php-parser", - "version": "v4.13.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "50953a2691a922aa1769461637869a0a2faa3f53" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53", - "reference": "50953a2691a922aa1769461637869a0a2faa3f53", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -1972,7 +2077,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1996,9 +2101,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2021-09-20T12:20:58+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "ondram/ci-detector", @@ -2074,37 +2179,39 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "4.3.70", + "version": "6.64.0.1", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "ec87deadc70f01c6d66ed22d653b7292cccdcbc9" + "reference": "d81b46b54e0d1431d2553c79763d937f99b965f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/ec87deadc70f01c6d66ed22d653b7292cccdcbc9", - "reference": "ec87deadc70f01c6d66ed22d653b7292cccdcbc9", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/d81b46b54e0d1431d2553c79763d937f99b965f4", + "reference": "d81b46b54e0d1431d2553c79763d937f99b965f4", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#0a73df114cdea7f30c8b5f6fbfbf8e6839a89e88", - "nikic/php-parser": "^4.13.0", - "php": ">=7.1.0" + "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "nikic/php-parser": "^5.6.1", + "php": "^7.4 || ^8.0" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" }, "require-dev": { - "phpstan/phpstan": "^0.12.49", - "phpunit/phpunit": "^7.5.18" + "doctrine/coding-standard": "^12.0.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.10.60", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^11.5.39", + "rector/rector": "1.2.10" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\BetterReflection\\": "src" @@ -2138,27 +2245,149 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/4.3.70" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.64.0.1" + }, + "time": "2025-09-19T20:49:30+00:00" + }, + { + "name": "ondrejmirtes/composer-attribute-collector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/composer-attribute-collector.git", + "reference": "48124ab07a5e19b7bd0ad20c791a8ff0bf3feab9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/composer-attribute-collector/zipball/48124ab07a5e19b7bd0ad20c791a8ff0bf3feab9", + "reference": "48124ab07a5e19b7bd0ad20c791a8ff0bf3feab9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "nikic/php-parser": "^5.4", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "composer/composer": ">=2.4", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.5" + }, + "type": "composer-plugin", + "extra": { + "class": "olvlvl\\ComposerAttributeCollector\\Plugin", + "composer-attribute-collector": { + "exclude": [ + "tests/Acme/PSR4/IncompatibleSignature.php" + ], + "include": [ + "tests" + ] + } + }, + "autoload": { + "psr-4": { + "olvlvl\\ComposerAttributeCollector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Olivier Laviale", + "email": "olivier.laviale@gmail.com", + "homepage": "https://olvlvl.com/", + "role": "Developer" + }, + { + "name": "Ondrej Mirtes", + "email": "ondrej@mirtes.cz", + "role": "Developer" + } + ], + "description": "A convenient and near zero-cost way to retrieve targets of PHP 8 attributes", + "support": { + "source": "https://github.com/ondrejmirtes/composer-attribute-collector/tree/1.1.1" + }, + "time": "2025-06-05T19:41:17+00:00" + }, + { + "name": "ondrejmirtes/php-merge", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/php-merge.git", + "reference": "43d30f9121c9a8762d4841a2350720a7fb2b6b44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/php-merge/zipball/43d30f9121c9a8762d4841a2350720a7fb2b6b44", + "reference": "43d30f9121c9a8762d4841a2350720a7fb2b6b44", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "sebastian/diff": "^2.0|^3.0|^4.0|^5.0|^6.0" + }, + "require-dev": { + "escapestudios/symfony2-coding-standard": "^3.5", + "phpstan/phpstan": "~1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpunit/phpunit": "~6|~7|~8|~9|~10", + "squizlabs/php_codesniffer": "~3", + "symplify/git-wrapper": "^9.1|^10.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "PhpMerge": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabian Bircher", + "email": "opensource@fabianbircher.com" + }, + { + "name": "Ondrej Mirtes", + "email": "ondrej@mirtes.cz" + } + ], + "description": "A PHP merge utility using the Diff php library or the command line git.", + "homepage": "https://github.com/bircher/php-merge", + "keywords": [ + "git", + "merge", + "php-merge" + ], + "support": { + "source": "https://github.com/ondrejmirtes/php-merge/tree/4.1.1" }, - "time": "2021-09-20T14:59:55+00:00" + "time": "2025-06-10T09:58:42+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.1.23", + "version": "0.4.32", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "3882ffe35d87a7eb7a133916907a44739d020184" + "reference": "47732ec27550617c93605006b7793793d4c85433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/3882ffe35d87a7eb7a133916907a44739d020184", - "reference": "3882ffe35d87a7eb7a133916907a44739d020184", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/47732ec27550617c93605006b7793793d4c85433", + "reference": "47732ec27550617c93605006b7793793d4c85433", "shasum": "" }, "type": "library", "autoload": { - "files": [ + "classmap": [ "Php8StubsMap.php" ] }, @@ -2170,41 +2399,39 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.1.23" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.32" }, - "time": "2021-08-31T00:08:55+00:00" + "time": "2025-09-23T00:21:29+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\PhpDocParser\\": [ @@ -2219,26 +2446,26 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "time": "2021-09-16T20:46:02+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -2267,31 +2494,31 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -2320,36 +2547,36 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", - "version": "1.1.3", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2359,7 +2586,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -2370,22 +2597,94 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/2.0.0" }, - "time": "2020-03-23T09:12:05+00:00" + "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.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/cache.git", - "reference": "44a568925556b0bd8cacc7b49fb0f1cf0d706a0c" + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/44a568925556b0bd8cacc7b49fb0f1cf0d706a0c", - "reference": "44a568925556b0bd8cacc7b49fb0f1cf0d706a0c", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { @@ -2393,7 +2692,7 @@ "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { @@ -2436,55 +2735,74 @@ ], "support": { "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.0" + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-18T12:12:35+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { "name": "react/child-process", - "version": "v0.6.1", + "version": "0.7.x-dev", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3" + "reference": "dfc3fe92ddf58646a2a17908180db712752f23a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", - "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/dfc3fe92ddf58646a2a17908180db712752f23a9", + "reference": "dfc3fe92ddf58646a2a17908180db712752f23a9", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/stream": "^1.0 || ^0.7.6" + "react/event-loop": "^1.2", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", - "react/socket": "^1.0", - "sebastian/environment": "^3.0 || ^2.0 || ^1.0" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], "description": "Event-driven library for executing child processes with ReactPHP.", "keywords": [ "event-driven", @@ -2493,39 +2811,45 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.1" + "source": "https://github.com/reactphp/child-process/tree/0.7.x" }, - "time": "2019-02-15T13:48:16+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-12-24T21:22:59+00:00" }, { "name": "react/dns", - "version": "v1.4.0", + "version": "v1.13.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", + "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.0 || ^0.5", - "react/promise": "^3.0 || ^2.7 || ^1.2.1", - "react/promise-timer": "^1.2" + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Dns\\": "src" + "React\\Dns\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2563,55 +2887,71 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.4.0" + "source": "https://github.com/reactphp/dns/tree/v1.13.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-18T12:12:55+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { "name": "react/event-loop", - "version": "v1.1.1", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13" + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", "autoload": { "psr-4": { - "React\\EventLoop\\": "src" + "React\\EventLoop\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ "asynchronous", @@ -2619,46 +2959,53 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, - "time": "2020-01-01T18:39:52+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" }, { "name": "react/http", - "version": "v1.1.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/reactphp/http.git", - "reference": "754b0c18545d258922ffa907f3b18598280fdecd" + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/754b0c18545d258922ffa907f3b18598280fdecd", - "reference": "754b0c18545d258922ffa907f3b18598280fdecd", + "url": "https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9", "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.0 || ^0.5", - "react/promise": "^2.3 || ^1.2.1", - "react/promise-stream": "^1.1", - "react/socket": "^1.6", - "react/stream": "^1.1", - "ringcentral/psr7": "^1.2" + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" }, "require-dev": { - "clue/block-react": "^1.1", - "clue/http-proxy-react": "^1.3", - "clue/reactphp-ssh-proxy": "^1.0", - "clue/socks-react": "^1.0", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Http\\": "src" + "React\\Http\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2703,48 +3050,45 @@ ], "support": { "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.1.0" + "source": "https://github.com/reactphp/http/tree/v1.11.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-11T11:01:51+00:00" + "time": "2024-11-20T15:24:08+00:00" }, { "name": "react/promise", - "version": "v2.8.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2753,169 +3097,74 @@ "authors": [ { "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.8.0" - }, - "time": "2020-05-12T15:16:56+00:00" - }, - { - "name": "react/promise-stream", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-stream.git", - "reference": "6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe", - "reference": "6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/promise": "^2.1 || ^1.2", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6" - }, - "require-dev": { - "clue/block-react": "^1.0", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", - "react/promise-timer": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\Stream\\": "src/" + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "The missing link between Promise-land and Stream-land for ReactPHP", - "homepage": "https://github.com/reactphp/promise-stream", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "Buffer", - "async", "promise", - "reactphp", - "stream", - "unwrap" + "promises" ], "support": { - "issues": "https://github.com/reactphp/promise-stream/issues", - "source": "https://github.com/reactphp/promise-stream/tree/v1.2.0" - }, - "time": "2019-07-03T12:29:10+00:00" - }, - { - "name": "react/promise-timer", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Christian Lück", - "email": "christian@lueck.tv" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", - "homepage": "https://github.com/reactphp/promise-timer", - "keywords": [ - "async", - "event-loop", - "promise", - "reactphp", - "timeout", - "timer" - ], - "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" - }, - "time": "2020-07-10T12:18:06+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "react/socket", - "version": "v1.6.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "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.1", - "react/event-loop": "^1.0 || ^0.5", - "react/promise": "^2.6.0 || ^1.2.1", - "react/promise-timer": "^1.4.0", - "react/stream": "^1.1" + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/promise-stream": "^1.2" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Socket\\": "src" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2954,53 +3203,71 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.6.0" + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-08-28T12:49:05+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { "name": "react/stream", - "version": "v1.1.1", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/reactphp/stream.git", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "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.0 || ^0.5 || ^0.4 || ^0.3.5" + "react/event-loop": "^1.2" }, "require-dev": { "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { "psr-4": { - "React\\Stream\\": "src" + "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", @@ -3014,109 +3281,125 @@ ], "support": { "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.1.1" + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, - "time": "2020-05-04T10:17:57+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" }, { - "name": "ringcentral/psr7", - "version": "1.3.0", + "name": "sebastian/diff", + "version": "6.0.2", "source": { "type": "git", - "url": "https://github.com/ringcentral/psr7.git", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ringcentral/psr7/zipball/360faaec4b563958b673fb52bbe94e37f14bc686", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { - "php": ">=5.3", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { - "psr-4": { - "RingCentral\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "PSR-7 message implementation", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "http", - "message", - "stream", - "uri" + "diff", + "udiff", + "unidiff", + "unified diff" ], "support": { - "source": "https://github.com/ringcentral/psr7/tree/master" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, - "time": "2018-05-29T20:21:04+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" }, { "name": "symfony/console", - "version": "v4.4.21", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", - "symfony/process": "<3.3" + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log": "For using the console logger", @@ -3149,8 +3432,14 @@ ], "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/v4.4.21" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -3166,32 +3455,38 @@ "type": "tidelift" } ], - "time": "2021-03-26T09:23:24+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { - "name": "symfony/finder", - "version": "v4.4.16", + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "26f63b8d4e92f2eecd90f6791a563ebb001abe31" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/26f63b8d4e92f2eecd90f6791a563ebb001abe31", - "reference": "26f63b8d4e92f2eecd90f6791a563ebb001abe31", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=8.1" }, "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" }, - "exclude-from-classmap": [ - "/Tests/" + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3200,18 +3495,18 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.16" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -3227,44 +3522,34 @@ "type": "tidelift" } ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "name": "symfony/finder", + "version": "v5.4.45", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "url": "https://github.com/symfony/finder.git", + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-mbstring": "For best performance" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Component\\Finder\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3273,25 +3558,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -3307,45 +3585,45 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.22.1", + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3353,24 +3631,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "ctype", "polyfill", - "portable", - "shim" + "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -3386,55 +3664,48 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -3444,16 +3715,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "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-php80/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -3469,39 +3742,45 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/service-contracts", - "version": "v1.1.8", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": "^7.1.3", - "psr/container": "^1.0" + "php": ">=7.2" }, "suggest": { - "symfony/service-implementation": "" + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.1-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3517,75 +3796,72 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v1.1.8" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, - "time": "2019-10-14T12:27:06+00:00" - } - ], - "packages-dev": [ + "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": "brianium/paratest", - "version": "v6.2.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "9a94366983ce32c7724fc92e3b544327d4adb9be" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9a94366983ce32c7724fc92e3b544327d4adb9be", - "reference": "9a94366983ce32c7724fc92e3b544327d4adb9be", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.5", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.1", - "sebastian/environment": "^5.1.3", - "symfony/console": "^4.4 || ^5.2", - "symfony/process": "^4.4 || ^5.2" + "ext-iconv": "*", + "php": ">=7.2" }, - "require-dev": { - "doctrine/coding-standard": "^8.2.0", - "ekino/phpstan-banned-code": "^0.3.1", - "ergebnis/phpstan-rules": "^0.15.3", - "ext-posix": "*", - "infection/infection": "^0.20.2", - "phpstan/phpstan": "^0.12.70", - "phpstan/phpstan-deprecation-rules": "^0.12.6", - "phpstan/phpstan-phpunit": "^0.12.17", - "phpstan/phpstan-strict-rules": "^0.12.9", - "squizlabs/php_codesniffer": "^3.5.8", - "symfony/filesystem": "^5.2.2", - "thecodingmachine/phpstan-strict-rules": "^0.12.1", - "vimeo/psalm": "^4.4.1" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, - "bin": [ - "bin/paratest" - ], "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "ParaTest\\": [ - "src/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3594,57 +3870,76 @@ ], "authors": [ { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "homepage": "http://brianscaturro.com", - "role": "Lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.2.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, - "time": "2021-01-29T15:25:31+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.4.0", + "name": "symfony/polyfill-php80", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^8.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3652,65 +3947,79 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "constructor", - "instantiate" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { - "name": "drupol/phposinfo", - "version": "1.6.5", + "name": "symfony/polyfill-php81", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/drupol/phposinfo.git", - "reference": "36b0250d38279c8a131a1898a31e359606024507" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupol/phposinfo/zipball/36b0250d38279c8a131a1898a31e359606024507", - "reference": "36b0250d38279c8a131a1898a31e359606024507", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">= 7.1.3" - }, - "require-dev": { - "drupol/php-conventions": "^1.7.1", - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "infection/infection": "^0.13.6 || ^0.15.0", - "phpspec/phpspec": "^5.1.2 || ^6.1.1" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "drupol\\phposinfo\\": "src/" - } + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3718,111 +4027,147 @@ ], "authors": [ { - "name": "Pol Dellaiera", - "email": "pol.dellaiera@protonmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Try to guess the host operating system.", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "operating system detection" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/drupol/phposinfo/issues", - "source": "https://github.com/drupol/phposinfo/tree/master" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { - "url": "https://github.com/drupol", + "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" } ], - "abandoned": "loophp/phposinfo", - "time": "2020-05-19T14:14:28+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.10.2", + "name": "symfony/polyfill-php83", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Symfony\\Polyfill\\Php83\\": "" }, - "files": [ - "src/DeepCopy/deep_copy.php" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "nategood/httpful", - "version": "0.2.20", + "name": "symfony/process", + "version": "v5.4.46", "source": { "type": "git", - "url": "https://github.com/nategood/httpful.git", - "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6" + "url": "https://github.com/symfony/process.git", + "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nategood/httpful/zipball/c1cd4d46a4b281229032cf39d4dd852f9887c0f6", - "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6", + "url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4", + "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "*" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { - "psr-0": { - "Httpful": "src/" - } + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3830,449 +4175,518 @@ ], "authors": [ { - "name": "Nate Good", - "email": "me@nategood.com", - "homepage": "http://nategood.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A Readable, Chainable, REST friendly, PHP HTTP Client", - "homepage": "http://github.com/nategood/httpful", - "keywords": [ - "api", - "curl", - "http", - "requests", - "rest", - "restful" - ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/nategood/httpful/issues", - "source": "https://github.com/nategood/httpful/tree/v0.2.20" + "source": "https://github.com/symfony/process/tree/v5.4.46" }, - "time": "2015-10-26T16:11:30+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T09:18:28+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.1", + "name": "symfony/service-contracts", + "version": "v2.5.4", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "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-master": "2.0.x-dev" + "dev-main": "2.5-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, - "time": "2020-06-27T14:33:11+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" }, { - "name": "phar-io/version", - "version": "3.1.0", + "name": "symfony/string", + "version": "v5.4.47", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "url": "https://github.com/symfony/string.git", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "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": { - "classmap": [ - "src/" + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library for handling version information and constraints", + "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": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, - "time": "2021-02-23T14:00:09+00:00" - }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-10T20:33:58+00:00" + } + ], + "packages-dev": [ { - "name": "php-parallel-lint/php-parallel-lint", - "version": "v1.2.0", + "name": "cweagans/composer-patches", + "version": "1.7.3", "source": { "type": "git", - "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", - "reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca" + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/474f18bc6cc6aca61ca40bfab55139de614e51ca", - "reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", "shasum": "" }, "require": { - "ext-json": "*", - "php": ">=5.4.0" - }, - "replace": { - "grogy/php-parallel-lint": "*", - "jakub-onderka/php-parallel-lint": "*" + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" }, "require-dev": { - "nette/tester": "^1.3 || ^2.0", - "php-parallel-lint/php-console-highlighter": "~0.3", - "squizlabs/php_codesniffer": "~3.0" + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" }, - "suggest": { - "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" }, - "bin": [ - "parallel-lint" - ], - "type": "library", "autoload": { - "classmap": [ - "./" - ] + "psr-4": { + "cweagans\\Composer\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "authors": [ { - "name": "Jakub Onderka", - "email": "ahoj@jakubonderka.cz" + "name": "Cameron Eagans", + "email": "me@cweagans.net" } ], - "description": "This tool check syntax of PHP files about 20x faster than serial check.", - "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", + "description": "Provides a way to patch Composer packages.", "support": { - "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", - "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/master" + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" }, - "time": "2020-04-04T12:18:32+00:00" + "time": "2022-12-20T22:53:13+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "myclabs/deep-copy", + "version": "1.13.4", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } + "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": { - "phpDocumentor\\Reflection\\": "src/" + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, - "time": "2020-06-27T09:03:43+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "name": "phar-io/manifest", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2" + "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": "5.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" }, { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2020-09-03T19:13:55+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "name": "phar-io/version", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "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": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "Library for handling version information and constraints", "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2020-09-17T18:55:26+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpspec/prophecy", - "version": "1.13.0", + "name": "php-parallel-lint/php-parallel-lint", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" }, "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" + "nette/tester": "^1.3 || ^2.0", + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } + "suggest": { + "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" }, + "bin": [ + "parallel-lint" + ], + "type": "library", "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "classmap": [ + "./src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-2-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "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": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" + "lint", + "static analysis" ], "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2024-03-27T12:14:49+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "dev-master", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "a6a1ed55749e2e39ad15b1976d523ba0af57f9c3" + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/a6a1ed55749e2e39ad15b1976d523ba0af57f9c3", - "reference": "a6a1ed55749e2e39ad15b1976d523ba0af57f9c3", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.15" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "rules.neon" @@ -4291,27 +4705,27 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/master" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" }, - "time": "2021-09-12T20:22:56+00:00" + "time": "2025-05-14T10:56:57+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "dev-master", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "f4654b27b107241e052755ec187a0b1964541ba6" + "reference": "9b629867b8e13e0afad8c8537b6541230d7b6a38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/f4654b27b107241e052755ec187a0b1964541ba6", - "reference": "f4654b27b107241e052755ec187a0b1964541ba6", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/9b629867b8e13e0afad8c8537b6541230d7b6a38", + "reference": "9b629867b8e13e0afad8c8537b6541230d7b6a38", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.12" }, "conflict": { "nette/application": "<2.3.0", @@ -4322,21 +4736,18 @@ "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", - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-php-parser": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", @@ -4356,95 +4767,41 @@ "description": "Nette Framework class reflection extension for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/master" - }, - "time": "2021-09-20T16:12:57+00:00" - }, - { - "name": "phpstan/phpstan-php-parser", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-php-parser.git", - "reference": "b4fe8703df4841a59fd61571687e3bebe896cd6d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-php-parser/zipball/b4fe8703df4841a59fd61571687e3bebe896cd6d", - "reference": "b4fe8703df4841a59fd61571687e3bebe896cd6d", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" - }, - "default-branch": true, - "type": "phpstan-extension", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP-Parser extensions for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-php-parser/issues", - "source": "https://github.com/phpstan/phpstan-php-parser/tree/master" + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.4" }, - "time": "2021-09-13T11:29:18+00:00" + "time": "2025-06-17T13:26:39+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "dev-master", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "5d501422a47e99f288a1eb07e1de141df0b1eab4" + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/5d501422a47e99f288a1eb07e1de141df0b1eab4", - "reference": "5d501422a47e99f288a1eb07e1de141df0b1eab4", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc", + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.18" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", @@ -4464,40 +4821,36 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/master" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.x" }, - "time": "2021-09-23T08:37:07+00:00" + "time": "2025-07-13T11:31:46+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "dev-master", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "b49efedea9da854d6c6d0cd6e7802ec8d76e63b4" + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b49efedea9da854d6c6d0cd6e7802ec8d76e63b4", - "reference": "b49efedea9da854d6c6d0cd6e7802ec8d76e63b4", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "rules.neon" @@ -4516,50 +4869,50 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/master" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" }, - "time": "2021-09-23T10:45:37+00:00" + "time": "2025-07-21T12:19:29+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", - "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" + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.5.2" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -4587,40 +4940,53 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "funding": [ { "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/php-code-coverage", + "type": "tidelift" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4647,7 +5013,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -4655,28 +5022,28 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcntl": "*" @@ -4684,7 +5051,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4710,7 +5077,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" }, "funding": [ { @@ -4718,32 +5086,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2024-07-03T05:07:44+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -4769,7 +5137,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" }, "funding": [ { @@ -4777,32 +5146,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2024-07-03T05:08:43+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -4828,7 +5197,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" }, "funding": [ { @@ -4836,59 +5206,52 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "9.5.4", + "version": "11.5.39", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" + "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", + "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", - "sebastian/version": "^3.0.2" - }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.11", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.2", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -4896,15 +5259,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-main": "11.5-dev" } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4927,44 +5290,57 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.39" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "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": "2021-03-23T07:16:29+00:00" + "time": "2025-09-14T06:20:41+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4987,7 +5363,8 @@ "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" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" }, "funding": [ { @@ -4995,32 +5372,32 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-07-03T04:41:36+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -5043,7 +5420,8 @@ "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" + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -5051,32 +5429,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -5098,7 +5476,8 @@ "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" + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" }, "funding": [ { @@ -5106,34 +5485,39 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -5172,41 +5556,54 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "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/sebastian/comparator", + "type": "tidelift" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -5229,73 +5626,8 @@ "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.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "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.4" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -5303,27 +5635,27 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -5331,7 +5663,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -5350,7 +5682,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -5358,42 +5690,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "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/sebastian/environment", + "type": "tidelift" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -5428,14 +5773,15 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" }, "funding": [ { @@ -5443,38 +5789,35 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2024-12-05T09:17:50+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5493,13 +5836,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://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.2" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -5507,33 +5851,33 @@ "type": "github" } ], - "time": "2020-10-26T15:55:19+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -5556,7 +5900,8 @@ "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" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -5564,34 +5909,34 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5613,7 +5958,8 @@ "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" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" }, "funding": [ { @@ -5621,32 +5967,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-07-03T05:00:13+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -5668,7 +6014,8 @@ "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" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { @@ -5676,32 +6023,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5719,74 +6066,20 @@ "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": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:17:30+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": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" }, "funding": [ { @@ -5794,32 +6087,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { "name": "sebastian/type", - "version": "2.3.1", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -5842,37 +6135,50 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "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/sebastian/type", + "type": "tidelift" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5895,7 +6201,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, "funding": [ { @@ -5903,479 +6210,308 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { - "name": "seld/jsonlint", - "version": "1.8.3", + "name": "shipmonk/composer-dependency-analyser", + "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10.63", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/name-collision-detector": "^2.0.0", + "slevomat/coding-standard": "^8.0.1" }, "bin": [ - "bin/jsonlint" + "bin/composer-dependency-analyser" ], "type": "library", "autoload": { "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "JSON Linter", + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", "keywords": [ - "json", - "linter", - "parser", - "validator" + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" ], "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" - } - ], - "time": "2020-11-11T09:19:24+00:00" + "time": "2024-08-08T08:12:32+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "name": "shipmonk/dead-code-detector", + "version": "0.12.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "url": "https://github.com/shipmonk-rnd/dead-code-detector.git", + "reference": "71b842269e9a29634e34074e723023e4e151518b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/shipmonk-rnd/dead-code-detector/zipball/71b842269e9a29634e34074e723023e4e151518b", + "reference": "71b842269e9a29634e34074e723023e4e151518b", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.9" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "composer-runtime-api": "^2.0", + "composer/semver": "^3.4", + "doctrine/orm": "^2.19 || ^3.0", + "editorconfig-checker/editorconfig-checker": "^10.6.0", + "ergebnis/composer-normalize": "^2.45.0", + "nette/application": "^3.1", + "nette/component-model": "^3.0", + "nette/utils": "^3.0 || ^4.0", + "nikic/php-parser": "^5.4.0", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "phpstan/phpstan-symfony": "^2.0.2", + "phpunit/phpunit": "^9.6.22", + "shipmonk/composer-dependency-analyser": "^1.8.2", + "shipmonk/name-collision-detector": "^2.1.1", + "shipmonk/phpstan-rules": "^4.1.0", + "slevomat/coding-standard": "^8.16.0", + "symfony/contracts": "^2.5 || ^3.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/routing": "^5.4 || ^6.0 || ^7.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0", + "twig/twig": "^3.0" }, - "type": "library", + "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "phpstan": { + "includes": [ + "rules.neon" + ] } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] + "ShipMonk\\PHPStan\\DeadCode\\": "src/" + } }, "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", + "description": "Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.", "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "PHPStan", + "dead code", + "static analysis", + "unused code" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + "issues": "https://github.com/shipmonk-rnd/dead-code-detector/issues", + "source": "https://github.com/shipmonk-rnd/dead-code-detector/tree/0.12.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": "2021-01-07T16:49:33+00:00" + "time": "2025-05-22T07:50:57+00:00" }, { - "name": "symfony/process", - "version": "v5.2.4", + "name": "shipmonk/name-collision-detector", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "url": "https://github.com/shipmonk-rnd/name-collision-detector.git", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/e8c8267a9a3774450b64f4cbf0bb035108e78f07", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "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": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "ShipMonk\\NameCollision\\": "src/" + } }, "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": "Simple tool to find ambiguous classes or any other name duplicates within your project.", + "keywords": [ + "ambiguous", + "autoload", + "autoloading", + "classname", + "collision", + "namespace" ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" + "issues": "https://github.com/shipmonk-rnd/name-collision-detector/issues", + "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.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": "2021-01-27T10:15:41+00:00" + "time": "2024-03-01T13:26:32+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.0", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "ext-dom": "*", "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" }, "type": "library", "autoload": { "classmap": [ - "src/" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" }, "funding": [ { - "url": "https://github.com/theseer", + "url": "https://github.com/staabm", "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" - }, - { - "name": "vaimo/composer-patches", - "version": "4.22.4", - "source": { - "type": "git", - "url": "https://github.com/vaimo/composer-patches.git", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vaimo/composer-patches/zipball/3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "drupol/phposinfo": "^1.6", - "ext-json": "*", - "php": ">=5.3.0", - "seld/jsonlint": "^1.7.1", - "vaimo/topological-sort": "^1.0" - }, - "require-dev": { - "composer/composer": "^1.0 || ^2.0", - "phpcompatibility/php-compatibility": ">=9.1.1", - "phpmd/phpmd": ">=2.6.0", - "sebastian/phpcpd": ">=1.4.3", - "squizlabs/php_codesniffer": ">=2.9.2", - "vaimo/composer-changelogs": "^0.17.0", - "vaimo/composer-patches-proxy": "1.0.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Vaimo\\ComposerPatches\\Plugin", - "changelog": { - "source": "changelog.json", - "output": { - "md": "CHANGELOG.md" - } - } - }, - "autoload": { - "psr-4": { - "Vaimo\\ComposerPatches\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Allan Paiste", - "email": "allan.paiste@vaimo.com" - } - ], - "description": "Applies a patch from a local or remote file to any package that is part of a given composer project. Patches can be defined both on project and on package level. Optional support for patch versioning, sequencing, custom patch applier configuration and patch command for testing/troubleshooting added patches.", - "keywords": [ - "Fixes", - "back-ports", - "backports", - "bulk patches", - "bundled patches", - "composer command", - "composer plugin", - "configurable patch applier", - "development patches", - "downloaded patches", - "environment flags", - "hot-fixes", - "hotfixes", - "indirect restrictions", - "maintenance", - "maintenance tools", - "multi-version patches", - "multiple formats", - "os-specific config", - "package bug-fix", - "package patches", - "patch branching", - "patch command", - "patch description", - "patch exclusion", - "patch header", - "patch meta-data", - "patch resolve", - "patch search", - "patch skipping", - "patcher", - "patching", - "plugin", - "remote patch files", - "resolve patches", - "skipped packages", - "tools", - "utilities", - "utility", - "utils", - "version restriction" - ], - "support": { - "docs": "https://github.com/vaimo/composer-patches", - "issues": "https://github.com/vaimo/composer-patches/issues", - "source": "https://github.com/vaimo/composer-patches" - }, - "time": "2021-02-25T11:24:50+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { - "name": "vaimo/topological-sort", - "version": "1.0.0", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/vaimo/topological-sort.git", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vaimo/topological-sort/zipball/e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "php": ">=5.3" - }, - "require-dev": { - "codeclimate/php-test-reporter": "dev-master", - "phpcompatibility/php-compatibility": "^9.1.1", - "phpmd/phpmd": "^2.6.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.9.2", - "symfony/console": "~2.5 || ~3.0 || ~4.0" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { - "psr-4": { - "Vaimo\\TopSort\\": "src/", - "Vaimo\\TopSort\\Tests\\": "tests/Tests/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Marc J. Schmidt", - "email": "marc@marcjschmidt.de" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "High-Performance TopSort/Dependency resolving algorithm (compatibility version to work with 5.3)", - "keywords": [ - "dependency resolving", - "topological sort", - "topsort" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "source": "https://github.com/vaimo/topological-sort/tree/1.0.0" - }, - "time": "2019-04-13T14:15:06+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "url": "https://github.com/theseer", + "type": "github" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -6386,11 +6522,12 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4 || ^8.0" + "php": "^8.2", + "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { - "php": "7.4.6" + "php": "8.2.99" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.6.0" } diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 0ef88e1e00..45d7f03d96 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,4 +1,15 @@ parameters: featureToggles: bleedingEdge: true - skipCheckGenericClasses: [] + checkNonStringableDynamicAccess: true + checkParameterCastableToNumberFunctions: true + skipCheckGenericClasses!: [] + stricterFunctionMap: true + reportPreciseLineForUnusedFunctionParameter: true + checkPrintfParameterTypes: true + internalTag: true + newStaticInAbstractClassStaticMethod: true + checkExtensionsForComparisonOperators: true + reportTooWideBool: true + rawMessageInBaseline: true + reportNestedTooWideType: false diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 64ec96e8cc..805ea348a9 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,218 +1,29 @@ parameters: customRulesetUsed: false -conditionalTags: - PHPStan\Rules\Properties\UninitializedPropertyRule: - phpstan.rules.rule: %checkUninitializedProperties% +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 0 -rules: - - PHPStan\Rules\Api\ApiInstantiationRule - - PHPStan\Rules\Api\ApiClassExtendsRule - - PHPStan\Rules\Api\ApiClassImplementsRule - - PHPStan\Rules\Api\ApiInterfaceExtendsRule - - PHPStan\Rules\Api\ApiMethodCallRule - - PHPStan\Rules\Api\ApiStaticCallRule - - PHPStan\Rules\Api\ApiTraitUseRule - - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule - - PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule - - PHPStan\Rules\Arrays\EmptyArrayItemRule - - PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule - - PHPStan\Rules\Cast\UnsetCastRule - - PHPStan\Rules\Classes\ClassAttributesRule - - PHPStan\Rules\Classes\ClassConstantAttributesRule - - PHPStan\Rules\Classes\ClassConstantRule - - PHPStan\Rules\Classes\DuplicateDeclarationRule - - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - - PHPStan\Rules\Classes\InstantiationRule - - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule - - PHPStan\Rules\Classes\NewStaticRule - - PHPStan\Rules\Classes\NonClassAttributeClassRule - - PHPStan\Rules\Classes\TraitAttributeClassRule - - PHPStan\Rules\Constants\FinalConstantRule - - PHPStan\Rules\Exceptions\ThrowExpressionRule - - PHPStan\Rules\Functions\ArrowFunctionAttributesRule - - PHPStan\Rules\Functions\ArrowFunctionReturnNullsafeByRefRule - - PHPStan\Rules\Functions\CallToFunctionParametersRule - - PHPStan\Rules\Functions\ClosureAttributesRule - - PHPStan\Rules\Functions\ExistingClassesInArrowFunctionTypehintsRule - - PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule - - PHPStan\Rules\Functions\ExistingClassesInTypehintsRule - - PHPStan\Rules\Functions\FunctionAttributesRule - - PHPStan\Rules\Functions\InnerFunctionRule - - PHPStan\Rules\Functions\ParamAttributesRule - - PHPStan\Rules\Functions\PrintfParametersRule - - PHPStan\Rules\Functions\ReturnNullsafeByRefRule - - PHPStan\Rules\Keywords\ContinueBreakInLoopRule - - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule - - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - - PHPStan\Rules\Methods\MissingMethodImplementationRule - - PHPStan\Rules\Methods\MethodAttributesRule - - PHPStan\Rules\Operators\InvalidAssignVarRule - - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule - - PHPStan\Rules\Properties\PropertyAttributesRule - - PHPStan\Rules\Properties\ReadOnlyPropertyRule - - PHPStan\Rules\Variables\UnsetRule - - PHPStan\Rules\Whitespace\FileWhitespaceRule +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% + PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule: + phpstan.rules.rule: %featureToggles.newStaticInAbstractClassStaticMethod% services: - - class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule - tags: - - phpstan.rules.rule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - - - - class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule - tags: - - phpstan.rules.rule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - - - - class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule - tags: - - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - - - - class: PHPStan\Rules\Methods\CallMethodsRule - tags: - - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - - - - class: PHPStan\Rules\Methods\CallStaticMethodsRule - tags: - - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - - - - class: PHPStan\Rules\Constants\OverridingConstantRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Methods\OverridingMethodRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Missing\MissingReturnRule - arguments: - checkExplicitMixedMissingReturn: %checkExplicitMixedMissingReturn% - checkPhpDocMissingReturn: %checkPhpDocMissingReturn% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Namespaces\ExistingNamesInGroupUseRule - tags: - - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - - - - class: PHPStan\Rules\Namespaces\ExistingNamesInUseRule - tags: - - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - - - - class: PHPStan\Rules\Operators\InvalidIncDecOperationRule - tags: - - phpstan.rules.rule - arguments: - checkThisOnly: %checkThisOnly% - - - - class: PHPStan\Rules\Properties\AccessPropertiesRule - tags: - - phpstan.rules.rule - arguments: - reportMagicProperties: %reportMagicProperties% - - - - class: PHPStan\Rules\Properties\AccessStaticPropertiesRule - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule - tags: - - phpstan.rules.rule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkThisOnly: %checkThisOnly% - - - - class: PHPStan\Rules\Properties\OverridingPropertyRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - reportMaybes: %reportMaybesInPropertyPhpDocTypes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\UninitializedPropertyRule - arguments: - additionalConstructors: %additionalConstructors% - - - - class: PHPStan\Rules\Properties\WritingToReadOnlyPropertiesRule - arguments: - checkThisOnly: %checkThisOnly% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\ReadingWriteOnlyPropertiesRule - arguments: - checkThisOnly: %checkThisOnly% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Variables\CompactVariablesRule - arguments: - checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule - - class: PHPStan\Rules\Variables\DefinedVariableRule - arguments: - cliArgumentsVariablesRegistered: %cliArgumentsVariablesRegistered% - checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension - - class: PHPStan\Rules\Regexp\RegularExpressionPatternRule - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension - - class: PHPStan\Rules\Classes\LocalTypeAliasesRule - arguments: - globalTypeAliases: %typeAliases% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension diff --git a/conf/config.level1.neon b/conf/config.level1.neon index 3b5f68d64a..9dc4f8bc67 100644 --- a/conf/config.level1.neon +++ b/conf/config.level1.neon @@ -7,10 +7,6 @@ parameters: reportMagicMethods: true reportMagicProperties: true -rules: - - PHPStan\Rules\Classes\UnusedConstructorParametersRule - - PHPStan\Rules\Constants\ConstantRule - - PHPStan\Rules\Functions\UnusedClosureUsesRule - - PHPStan\Rules\Variables\EmptyRule - - PHPStan\Rules\Variables\IssetRule - - PHPStan\Rules\Variables\NullCoalesceRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 1 diff --git a/conf/config.level10.neon b/conf/config.level10.neon new file mode 100644 index 0000000000..d5cb28adcc --- /dev/null +++ b/conf/config.level10.neon @@ -0,0 +1,9 @@ +includes: + - config.level9.neon + +parameters: + checkImplicitMixed: true + +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 10 diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 0a4e60096b..dd50138b54 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -6,61 +6,19 @@ parameters: checkThisOnly: false checkPhpDocMissingReturn: true -rules: - - PHPStan\Rules\Cast\EchoRule - - PHPStan\Rules\Cast\InvalidCastRule - - PHPStan\Rules\Cast\InvalidPartOfEncapsedStringRule - - PHPStan\Rules\Cast\PrintRule - - PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule - - PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule - - PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule - - PHPStan\Rules\Generics\ClassTemplateTypeRule - - PHPStan\Rules\Generics\FunctionTemplateTypeRule - - PHPStan\Rules\Generics\FunctionSignatureVarianceRule - - PHPStan\Rules\Generics\InterfaceTemplateTypeRule - - PHPStan\Rules\Generics\MethodTemplateTypeRule - - PHPStan\Rules\Generics\MethodSignatureVarianceRule - - PHPStan\Rules\Generics\TraitTemplateTypeRule - - PHPStan\Rules\Generics\UsedTraitsRule - - PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - - PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule - - PHPStan\Rules\Operators\InvalidBinaryOperationRule - - PHPStan\Rules\Operators\InvalidUnaryOperationRule - - PHPStan\Rules\Operators\InvalidComparisonOperationRule - - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - - PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule - - PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule - - PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule - - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 2 + +conditionalTags: + PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension: + phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension: + phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag% services: - - class: PHPStan\Rules\Classes\MixinRule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Functions\CallCallablesRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Generics\ClassAncestorsRule - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Generics\InterfaceAncestorsRule - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension + - - class: PHPStan\Rules\PhpDoc\InvalidPhpDocVarTagTypeRule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkMissingVarTagTypehint: %checkMissingVarTagTypehint% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 4f6affaa1f..b040bf3aeb 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -1,88 +1,9 @@ includes: - config.level2.neon -rules: - - PHPStan\Rules\Arrays\AppendedArrayItemTypeRule - - PHPStan\Rules\Arrays\ArrayDestructuringRule - - PHPStan\Rules\Arrays\IterableInForeachRule - - PHPStan\Rules\Arrays\OffsetAccessAssignmentRule - - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule - - PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule - - PHPStan\Rules\Arrays\UnpackIterableInArrayRule - - PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule - - PHPStan\Rules\Functions\ClosureReturnTypeRule - - PHPStan\Rules\Generators\YieldTypeRule - - PHPStan\Rules\Methods\ReturnTypeRule - - PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule - - PHPStan\Rules\Properties\TypesAssignedToPropertiesRule - - PHPStan\Rules\Variables\ThrowTypeRule - - PHPStan\Rules\Variables\VariableCloningRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 3 parameters: checkPhpDocMethodSignatures: true - -services: - - - class: PHPStan\Rules\Arrays\AppendedArrayKeyTypeRule - arguments: - checkUnionTypes: %checkUnionTypes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule - arguments: - 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 - arguments: - functionReflector: @betterReflectionFunctionReflector - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Generators\YieldFromTypeRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Generators\YieldInGeneratorRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 61f32bd2f7..cffef56a11 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -1,164 +1,27 @@ includes: - config.level3.neon -rules: - - PHPStan\Rules\Arrays\DeadForeachRule - - PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule - - PHPStan\Rules\DeadCode\NoopRule - - PHPStan\Rules\DeadCode\UnreachableStatementRule - - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule - - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - - PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule - - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule - - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToMethodStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToStaticMethodStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\NullsafeMethodCallRule - - PHPStan\Rules\Properties\NullsafePropertyFetchRule - - PHPStan\Rules\TooWideTypehints\TooWideArrowFunctionReturnTypehintRule - - PHPStan\Rules\TooWideTypehints\TooWideClosureReturnTypehintRule - - PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] and #[RegisteredCollector] attributes + level: 4 + +conditionalTags: + PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% parameters: checkAdvancedIsset: true services: - - class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule - arguments: - checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule - - class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule - - class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\BooleanNotConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule - arguments: - alwaysWrittenTags: %propertyAlwaysWrittenTags% - alwaysReadTags: %propertyAlwaysReadTags% - checkUninitializedProperties: %checkUninitializedProperties% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\IfConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeFunctionCallRule - arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule - arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule - arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\MatchExpressionRule - arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule - arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\UnreachableIfBranchesRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\UnreachableTernaryElseBranchRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\WhileLoopAlwaysFalseConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\WhileLoopAlwaysTrueConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule - arguments: - checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index c890be88ec..b4518ba7e2 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -5,14 +5,20 @@ parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true -rules: - - PHPStan\Rules\DateTimeInstantiationRule - - PHPStan\Rules\Functions\ImplodeFunctionRule +conditionalTags: + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% + PHPStan\Rules\Functions\PrintfParameterTypeRule: + phpstan.rules.rule: %featureToggles.checkPrintfParameterTypes% + +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 5 services: - - class: PHPStan\Rules\Functions\RandomIntParametersRule + class: PHPStan\Rules\Functions\ParameterCastableToNumberRule + - + class: PHPStan\Rules\Functions\PrintfParameterTypeRule arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule + checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes% diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 05f3616832..9da68b2d78 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -2,15 +2,9 @@ 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\Properties\MissingPropertyTypehintRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 6 diff --git a/conf/config.level7.neon b/conf/config.level7.neon index af8dbbe8d7..9ed0bc548f 100644 --- a/conf/config.level7.neon +++ b/conf/config.level7.neon @@ -4,3 +4,7 @@ includes: parameters: checkUnionTypes: true reportMaybes: true + +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 7 diff --git a/conf/config.level8.neon b/conf/config.level8.neon index fd1dbcd17c..991a4bf129 100644 --- a/conf/config.level8.neon +++ b/conf/config.level8.neon @@ -3,3 +3,7 @@ includes: parameters: checkNullables: true + +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 8 diff --git a/conf/config.level9.neon b/conf/config.level9.neon index efb3e30ffa..9c31364230 100644 --- a/conf/config.level9.neon +++ b/conf/config.level9.neon @@ -3,3 +3,7 @@ includes: parameters: checkExplicitMixed: true + +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 9 diff --git a/conf/config.levelmax.neon b/conf/config.levelmax.neon index da48578fe3..ce4c43f2f7 100644 --- a/conf/config.levelmax.neon +++ b/conf/config.levelmax.neon @@ -1,2 +1,2 @@ includes: - - config.level9.neon + - config.level10.neon diff --git a/conf/config.neon b/conf/config.neon index f74919aafc..6f4076a3dd 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,40 +1,52 @@ includes: - - config.stubFiles.neon + - parametersSchema.neon + - services.neon + - parsers.neon + parameters: bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php - ../stubs/runtime/ReflectionAttribute.php - - ../stubs/runtime/Attribute.php - excludes_analyse: [] - excludePaths: null + - ../stubs/runtime/Attribute85.php + - ../stubs/runtime/ReflectionIntersectionType.php + excludePaths: [] level: null paths: [] exceptions: implicitThrows: true + reportUncheckedExceptionDeadCatch: true uncheckedExceptionRegexes: [] uncheckedExceptionClasses: [] checkedExceptionRegexes: [] checkedExceptionClasses: [] check: missingCheckedExceptionInThrows: false - tooWideThrowType: false + tooWideThrowType: true featureToggles: bleedingEdge: false - disableRuntimeReflectionProvider: false - skipCheckGenericClasses: [] + checkNonStringableDynamicAccess: false + checkParameterCastableToNumberFunctions: false + skipCheckGenericClasses: + - DOMNamedNodeMap + stricterFunctionMap: false + reportPreciseLineForUnusedFunctionParameter: false + checkPrintfParameterTypes: false + internalTag: false + newStaticInAbstractClassStaticMethod: false + checkExtensionsForComparisonOperators: false + reportTooWideBool: false + rawMessageInBaseline: false + reportNestedTooWideType: false fileExtensions: - php checkAdvancedIsset: false - checkAlwaysTrueCheckTypeFunctionCall: false - checkAlwaysTrueInstanceof: false - checkAlwaysTrueStrictComparison: false + reportAlwaysTrueInLastCondition: false checkClassCaseSensitivity: false checkExplicitMixed: false + checkImplicitMixed: false checkFunctionArgumentTypes: false checkFunctionNameCase: false - checkGenericClassInNonGenericObjectType: false checkInternalClassCaseSensitivity: false - checkMissingIterableValueType: false checkMissingCallableSignature: false checkMissingVarTagTypehint: false checkArgumentsPassedByReference: false @@ -42,18 +54,29 @@ parameters: checkNullables: false checkThisOnly: true checkUnionTypes: false + checkBenevolentUnionTypes: false checkExplicitMixedMissingReturn: false checkPhpDocMissingReturn: false checkPhpDocMethodSignatures: false checkExtraArguments: false checkMissingTypehints: false + checkTooWideParameterOutInProtectedAndPublicMethods: false checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false + checkDynamicProperties: false + strictRulesInstalled: false + deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false + checkStrictPrintfPlaceholderTypes: false reportMaybes: false reportMaybesInMethodSignatures: false reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false + reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + reportPossiblyNonexistentGeneralArrayOffset: false + reportPossiblyNonexistentConstantArrayOffset: false + checkMissingOverrideMethodAttribute: false mixinExcludeClasses: [] scanFiles: [] scanDirectories: [] @@ -66,37 +89,70 @@ parameters: phpVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true + polluteScopeWithBlock: true propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] additionalConstructors: [] treatPhpDocTypesAsCertain: true + usePathConstantsAsConstantString: false + rememberPossiblyImpureFunctionValues: true + tips: + discoveringSymbols: true + treatPhpDocTypesAsCertain: true tipsOfTheDay: true reportMagicMethods: false reportMagicProperties: false ignoreErrors: [] internalErrorsCountLimit: 50 cache: - nodesByFileCountMax: 1024 - nodesByStringCountMax: 1024 + nodesByStringCountMax: 256 reportUnmatchedIgnoredErrors: true - scopeClass: PHPStan\Analyser\MutatingScope typeAliases: [] universalObjectCratesClasses: - stdClass + stubFiles: + - ../stubs/ReflectionAttribute.stub + - ../stubs/ReflectionClassConstant.stub + - ../stubs/ReflectionFunctionAbstract.stub + - ../stubs/ReflectionMethod.stub + - ../stubs/ReflectionParameter.stub + - ../stubs/ReflectionProperty.stub + - ../stubs/iterable.stub + - ../stubs/ArrayObject.stub + - ../stubs/WeakReference.stub + - ../stubs/ext-ds.stub + - ../stubs/ImagickPixel.stub + - ../stubs/PDOStatement.stub + - ../stubs/date.stub + - ../stubs/ibm_db2.stub + - ../stubs/mysqli.stub + - ../stubs/zip.stub + - ../stubs/dom.stub + - ../stubs/spl.stub + - ../stubs/SplObjectStorage.stub + - ../stubs/Exception.stub + - ../stubs/arrayFunctions.stub + - ../stubs/core.stub + - ../stubs/typeCheckingFunctions.stub + - ../stubs/Countable.stub + - ../stubs/file.stub + - ../stubs/stream_socket_client.stub + - ../stubs/stream_socket_server.stub earlyTerminatingMethodCalls: [] earlyTerminatingFunctionCalls: [] - memoryLimitFile: %tmpDir%/.memory_limit - tempResultCachePath: %tmpDir%/resultCaches resultCachePath: %tmpDir%/resultCache.php + resultCacheSkipIfOlderThanDays: 7 resultCacheChecksProjectExtensionFilesDependencies: false - staticReflectionClassNamePatterns: - - '#^PhpParser\\#' - - '#^PHPStan\\#' - - '#^Hoa\\#' 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 @@ -139,592 +195,56 @@ parameters: - OPENSSL_VERSION_NUMBER - ZEND_DEBUG_BUILD - ZEND_THREAD_SAFE + - E_ALL # different on PHP 8.4 + customRulesetUsed: null editorUrl: null + editorUrlTitle: null + errorFormat: null + sysGetTempDir: ::sys_get_temp_dir() + sourceLocatorPlaygroundMode: false + pro: + dnsServers: + - '1.1.1.2' + tmpDir: %sysGetTempDir%/phpstan-fixer + __validate: true + parametersNotInvalidatingCache: + - [parameters, editorUrl] + - [parameters, editorUrlTitle] + - [parameters, errorFormat] + - [parameters, ignoreErrors] + - [parameters, reportUnmatchedIgnoredErrors] + - [parameters, tipsOfTheDay] + - [parameters, parallel] + - [parameters, internalErrorsCountLimit] + - [parameters, cache] + - [parameters, memoryLimitFile] + - [parameters, pro] + - parametersSchema extensions: rules: PHPStan\DependencyInjection\RulesExtension + expandRelativePaths: PHPStan\DependencyInjection\ExpandRelativePathExtension conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension + validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension + validateExcludePaths: PHPStan\DependencyInjection\ValidateExcludePathsExtension + autowiredAttributeServices: PHPStan\DependencyInjection\AutowiredAttributeServicesExtension + validateServiceTags: PHPStan\DependencyInjection\ValidateServiceTagsExtension -parametersSchema: - bootstrapFiles: listOf(string()) - excludes_analyse: listOf(string()) - excludePaths: schema(anyOf( - structure([ - analyse: listOf(string()), - ]), - structure([ - analyseAndScan: listOf(string()), - ]) - structure([ - analyse: listOf(string()), - analyseAndScan: listOf(string()) - ]) - ), nullable()) - level: schema(anyOf(int(), string()), nullable()) - paths: listOf(string()) - exceptions: structure([ - implicitThrows: bool(), - uncheckedExceptionRegexes: listOf(string()), - uncheckedExceptionClasses: listOf(string()), - checkedExceptionRegexes: listOf(string()), - checkedExceptionClasses: listOf(string()), - check: structure([ - missingCheckedExceptionInThrows: bool(), - tooWideThrowType: bool() - ]) - ]) - featureToggles: structure([ - bleedingEdge: bool(), - disableRuntimeReflectionProvider: bool(), - skipCheckGenericClasses: listOf(string()), - ]) - fileExtensions: listOf(string()) - checkAdvancedIsset: bool() - checkAlwaysTrueCheckTypeFunctionCall: bool() - checkAlwaysTrueInstanceof: bool() - checkAlwaysTrueStrictComparison: bool() - checkClassCaseSensitivity: bool() - checkExplicitMixed: bool() - checkFunctionArgumentTypes: bool() - checkFunctionNameCase: bool() - checkGenericClassInNonGenericObjectType: bool() - checkInternalClassCaseSensitivity: bool() - checkMissingIterableValueType: bool() - checkMissingCallableSignature: bool() - checkMissingVarTagTypehint: bool() - checkArgumentsPassedByReference: bool() - checkMaybeUndefinedVariables: bool() - checkNullables: bool() - checkThisOnly: bool() - checkUnionTypes: bool() - checkExplicitMixedMissingReturn: bool() - checkPhpDocMissingReturn: bool() - checkPhpDocMethodSignatures: bool() - checkExtraArguments: bool() - checkMissingTypehints: bool() - checkTooWideReturnTypesInProtectedAndPublicMethods: bool() - checkUninitializedProperties: bool() - inferPrivatePropertyTypeFromConstructor: bool() - - tipsOfTheDay: bool() - reportMaybes: bool() - reportMaybesInMethodSignatures: bool() - reportMaybesInPropertyPhpDocTypes: bool() - reportStaticMethodSignatures: bool() - parallel: structure([ - jobSize: int(), - processTimeout: float(), - maximumNumberOfProcesses: int(), - minimumNumberOfJobsPerProcess: int(), - buffer: int() - ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80099))), nullable()) - polluteScopeWithLoopInitialAssignments: bool() - polluteScopeWithAlwaysIterableForeach: bool() - propertyAlwaysWrittenTags: listOf(string()) - propertyAlwaysReadTags: listOf(string()) - additionalConstructors: listOf(string()) - treatPhpDocTypesAsCertain: bool() - reportMagicMethods: bool() - reportMagicProperties: bool() - ignoreErrors: listOf( - anyOf( - string(), - structure([ - message: string() - path: string() - ]), - structure([ - message: string() - count: int() - path: string() - ]), - structure([ - message: string() - paths: listOf(string()) - ]) - ) - ) - internalErrorsCountLimit: int() - cache: structure([ - nodesByFileCountMax: int() - nodesByStringCountMax: int() - ]) - reportUnmatchedIgnoredErrors: bool() - scopeClass: string() - typeAliases: arrayOf(string()) - universalObjectCratesClasses: listOf(string()) - stubFiles: listOf(string()) - earlyTerminatingMethodCalls: arrayOf(listOf(string())) - earlyTerminatingFunctionCalls: listOf(string()) - memoryLimitFile: string() - tempResultCachePath: string() - resultCachePath: string() - resultCacheChecksProjectExtensionFilesDependencies: bool() - staticReflectionClassNamePatterns: listOf(string()) - dynamicConstantNames: listOf(string()) - customRulesetUsed: bool() - rootDir: string() - tmpDir: string() - currentWorkingDirectory: string() - cliArgumentsVariablesRegistered: bool() - mixinExcludeClasses: listOf(string()) - scanFiles: listOf(string()) - scanDirectories: listOf(string()) - fixerTmpDir: string() - editorUrl: schema(string(), nullable()) - - # irrelevant Nette parameters - debugMode: bool() - productionMode: bool() - tempDir: string() - - # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: listOf(string()) - generateBaselineFile: schema(string(), nullable()) - analysedPaths: listOf(string()) - composerAutoloaderProjectPaths: listOf(string()) - analysedPathsFromConfig: listOf(string()) - usedLevel: string() - cliAutoloadFile: schema(string(), nullable()) - - # internal - static reflection - singleReflectionFile: schema(string(), nullable()) - singleReflectionInsteadOfFile: schema(string(), nullable()) - -rules: - - PHPStan\Rules\Debug\DumpTypeRule - - PHPStan\Rules\Debug\FileAssertRule +autowiredAttributeServices: + level: null conditionalTags: PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% - PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule: - phpstan.rules.rule: %exceptions.check.tooWideThrowType% - PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule: - phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule: + phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% + PHPStan\Rules\Properties\UninitializedPropertyRule: + phpstan.rules.rule: %checkUninitializedProperties% services: - - - class: PhpParser\BuilderFactory - - - - class: PHPStan\Parser\LexerFactory - - - - class: PhpParser\NodeVisitor\NameResolver - - - - class: PhpParser\NodeVisitor\NodeConnectingVisitor - - - - class: PhpParser\PrettyPrinter\Standard - - - - class: PHPStan\Broker\AnonymousClassNameHelper - arguments: - relativePathHelper: @simpleRelativePathHelper - - - - class: PHPStan\Php\PhpVersion - factory: @PHPStan\Php\PhpVersionFactory::create - - - - class: PHPStan\Php\PhpVersionFactory - factory: @PHPStan\Php\PhpVersionFactoryFactory::create - - - - class: PHPStan\Php\PhpVersionFactoryFactory - arguments: - versionId: %phpVersion% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - - - - class: PHPStan\PhpDocParser\Lexer\Lexer - - - - class: PHPStan\PhpDocParser\Parser\TypeParser - - - - class: PHPStan\PhpDocParser\Parser\ConstExprParser - - - - class: PHPStan\PhpDocParser\Parser\PhpDocParser - - - - class: PHPStan\PhpDoc\PhpDocInheritanceResolver - - - - class: PHPStan\PhpDoc\PhpDocNodeResolver - - - - class: PHPStan\PhpDoc\PhpDocStringResolver - - - - class: PHPStan\PhpDoc\ConstExprNodeResolver - - - - class: PHPStan\PhpDoc\TypeNodeResolver - - - - class: PHPStan\PhpDoc\TypeNodeResolverExtensionRegistryProvider - factory: PHPStan\PhpDoc\LazyTypeNodeResolverExtensionRegistryProvider - - - - class: PHPStan\PhpDoc\TypeStringResolver - - - - class: PHPStan\PhpDoc\StubValidator - - - - class: PHPStan\Analyser\Analyser - arguments: - internalErrorsCountLimit: %internalErrorsCountLimit% - - - - class: PHPStan\Analyser\FileAnalyser - arguments: - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - - - class: PHPStan\Analyser\IgnoredErrorHelper - arguments: - ignoreErrors: %ignoreErrors% - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - - - class: PHPStan\Analyser\LazyScopeFactory - arguments: - scopeClass: %scopeClass% - autowired: - - PHPStan\Analyser\ScopeFactory - - - - class: PHPStan\Analyser\NodeScopeResolver - arguments: - classReflector: @nodeScopeResolverClassReflector - polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments% - polluteScopeWithAlwaysIterableForeach: %polluteScopeWithAlwaysIterableForeach% - earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls% - earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% - implicitThrows: %exceptions.implicitThrows% - - - - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory - arguments: - scanFileFinder: @fileFinderScan - cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% - analysedPaths: %analysedPaths% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - stubFiles: %stubFiles% - usedLevel: %usedLevel% - cliAutoloadFile: %cliAutoloadFile% - bootstrapFiles: %bootstrapFiles% - scanFiles: %scanFiles% - scanDirectories: %scanDirectories% - checkDependenciesOfProjectExtensionFiles: %resultCacheChecksProjectExtensionFilesDependencies% - - - - class: PHPStan\Analyser\ResultCache\ResultCacheClearer - arguments: - cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% - - - - class: PHPStan\Cache\Cache - arguments: - storage: @cacheStorage - - - - class: PHPStan\Command\AnalyseApplication - arguments: - memoryLimitFile: %memoryLimitFile% - internalErrorsCountLimit: %internalErrorsCountLimit% - - - - class: PHPStan\Command\AnalyserRunner - - - - class: PHPStan\Command\FixerApplication - arguments: - analysedPaths: %analysedPaths% - currentWorkingDirectory: %currentWorkingDirectory% - fixerTmpDir: %fixerTmpDir% - maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% - - - - class: PHPStan\Command\IgnoredRegexValidator - arguments: - parser: @regexParser - - - - class: PHPStan\Dependency\DependencyResolver - - - - class: PHPStan\Dependency\ExportedNodeFetcher - - - - class: PHPStan\Dependency\ExportedNodeResolver - - - - class: PHPStan\Dependency\ExportedNodeVisitor - - - - class: PHPStan\DependencyInjection\Container - factory: PHPStan\DependencyInjection\MemoizingContainer - arguments: - originalContainer: @PHPStan\DependencyInjection\Nette\NetteContainer - - - - class: PHPStan\DependencyInjection\Nette\NetteContainer - autowired: - - PHPStan\DependencyInjection\Nette\NetteContainer - - - - class: PHPStan\DependencyInjection\DerivativeContainerFactory - arguments: - currentWorkingDirectory: %currentWorkingDirectory% - tempDirectory: %tempDir% - additionalConfigFiles: %additionalConfigFiles% - analysedPaths: %analysedPaths% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - analysedPathsFromConfig: %analysedPathsFromConfig% - usedLevel: %usedLevel% - generateBaselineFile: %generateBaselineFile% - - - - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Reflection\LazyClassReflectionExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider - factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider - - - - class: PHPStan\File\FileHelper - arguments: - workingDirectory: %currentWorkingDirectory% - - - - class: PHPStan\File\FileExcluderFactory - arguments: - obsoleteExcludesAnalyse: %excludes_analyse% - excludePaths: %excludePaths% - - - - implement: PHPStan\File\FileExcluderRawFactory - arguments: - stubFiles: %stubFiles% - - 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\NodeVisitor\StatementOrderVisitor - - - - class: PHPStan\Parallel\ParallelAnalyser - arguments: - internalErrorsCountLimit: %internalErrorsCountLimit% - processTimeout: %parallel.processTimeout% - decoderBufferSize: %parallel.buffer% - - - - class: PHPStan\Parallel\Scheduler - arguments: - jobSize: %parallel.jobSize% - maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% - minimumNumberOfJobsPerProcess: %parallel.minimumNumberOfJobsPerProcess% - - - - class: PHPStan\Parser\CachedParser - arguments: - originalParser: @pathRoutingParser - cachedNodesByStringCountMax: %cache.nodesByStringCountMax% - - - - class: PHPStan\Parser\FunctionCallStatementFinder - - - - class: PHPStan\Process\CpuCoreCounter - - - - implement: PHPStan\Reflection\FunctionReflectionFactory - - - - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension - - - - class: PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory - arguments: - fileFinder: @fileFinderScan - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository - - - - implement: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory - - - - implement: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorFactory - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository - - - - class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension - tags: - - phpstan.broker.methodsClassReflectionExtension - arguments: - mixinExcludeClasses: %mixinExcludeClasses% - - - - class: PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension - tags: - - phpstan.broker.propertiesClassReflectionExtension - arguments: - mixinExcludeClasses: %mixinExcludeClasses% - - - - class: PHPStan\Reflection\Php\PhpClassReflectionExtension - arguments: - inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - universalObjectCratesClasses: %universalObjectCratesClasses% - - - - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory - - - - class: PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension - tags: - - phpstan.broker.methodsClassReflectionExtension - - - - class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension - tags: - - phpstan.broker.propertiesClassReflectionExtension - arguments: - classes: %universalObjectCratesClasses% - - - - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider - factory: PHPStan\Reflection\ReflectionProvider\LazyReflectionProviderProvider - - - - class: PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider - arguments: - functionReflector: @betterReflectionFunctionReflector - - - - class: PHPStan\Reflection\SignatureMap\SignatureMapParser - - - - class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider - autowired: - - PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider - - - - class: PHPStan\Reflection\SignatureMap\Php8SignatureMapProvider - autowired: - - PHPStan\Reflection\SignatureMap\Php8SignatureMapProvider - - - - class: PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory - - - - class: PHPStan\Reflection\SignatureMap\SignatureMapProvider - factory: @PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory::create() - - - - class: PHPStan\Rules\Api\ApiRuleHelper - - - - class: PHPStan\Rules\AttributesCheck - - - - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck - arguments: - reportMaybes: %reportMaybes% - - - - class: PHPStan\Rules\ClassCaseSensitivityCheck - arguments: - checkInternalClassCaseSensitivity: %checkInternalClassCaseSensitivity% - - - - 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 @@ -732,986 +252,12 @@ services: class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule - - class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInThrowsCheck - arguments: - exceptionTypeResolver: @exceptionTypeResolver - - - - class: PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule - - - - class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule - - - - class: PHPStan\Rules\Exceptions\TooWideThrowTypeCheck - - - - 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\Generics\CrossCheckInterfacesHelper - - - - class: PHPStan\Rules\Generics\GenericAncestorsCheck - arguments: - checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% - skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% - - - - class: PHPStan\Rules\Generics\GenericObjectTypeCheck - - - - class: PHPStan\Rules\Generics\TemplateTypeCheck - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - - - - class: PHPStan\Rules\Generics\VarianceCheck - - - - class: PHPStan\Rules\IssetCheck - arguments: - checkAdvancedIsset: %checkAdvancedIsset% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - - - - # checked as part of OverridingMethodRule - class: PHPStan\Rules\Methods\MethodSignatureRule - arguments: - reportMaybes: %reportMaybesInMethodSignatures% - reportStatic: %reportStaticMethodSignatures% - - - - 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\PhpDoc\UnresolvableTypeHelper - - - - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider - - - - class: PHPStan\Rules\Properties\PropertyDescriptor - - - - class: PHPStan\Rules\Properties\PropertyReflectionFinder - - - - class: PHPStan\Rules\RegistryFactory - - - - class: PHPStan\Rules\RuleLevelHelper - arguments: - checkNullables: %checkNullables% - checkThisOnly: %checkThisOnly% - checkUnionTypes: %checkUnionTypes% - checkExplicitMixed: %checkExplicitMixed% - - - - class: PHPStan\Rules\UnusedFunctionParametersCheck - - - - class: PHPStan\Type\FileTypeMapper - - - - class: PHPStan\Type\TypeAliasResolver - arguments: - globalTypeAliases: %typeAliases% - - - - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension - 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: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayFillKeysFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayFlipFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayKeyExistsFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\ArrayKeyFirstDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayKeyLastDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule - - class: PHPStan\Type\Php\ArrayKeysFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - 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\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\ArrayValuesFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySumFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - 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\CountFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\CountFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\CurlInitReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateIntervalConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DateTimeDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateTimeConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DsMapDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ExplodeFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension - 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\GetoptFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\HashHmacFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\HashFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\IntdivThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - - - class: PHPStan\Type\Php\JsonThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionFunctionConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionMethodConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionPropertyConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension - tags: - - phpstan.broker.propertiesClassReflectionExtension - - - - class: PHPStan\Type\Php\SimpleXMLElementConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\StatDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\MethodExistsTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\PropertyExistsTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\MinMaxFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\NumberFormatFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\PathinfoFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\PregSplitDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ReflectionClassIsSubclassOfTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayPointerFunctionsDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\VarExportFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\MbFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\MbConvertEncodingFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\MbSubstituteCharacterDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\HrtimeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ImplodeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\NonEmptyStringFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrRepeatFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\PowFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrtotimeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\RandomIntFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\RangeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\AssertFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\ClassExistsFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\DefinedConstantTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsIntFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsFloatFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNullFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsBoolFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsCountableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsResourceFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsStringFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsObjectFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNumericFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsScalarFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\TypeSpecifyingFunctionsDynamicReturnTypeExtension - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - universalObjectCratesClasses: %universalObjectCratesClasses% - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\SimpleXMLElementAsXMLMethodReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\SimpleXMLElementXpathMethodReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\StrSplitFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrTokFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\SprintfFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrvalFamilyFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - 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\ReflectionGetAttributesMethodReturnTypeExtension - arguments: - className: ReflectionClass - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension - arguments: - className: ReflectionClassConstant - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension - arguments: - className: ReflectionFunctionAbstract - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension - arguments: - className: ReflectionParameter - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension - arguments: - className: ReflectionProperty - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - exceptionTypeResolver: - class: PHPStan\Rules\Exceptions\ExceptionTypeResolver - factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver - - typeSpecifier: - class: PHPStan\Analyser\TypeSpecifier - factory: @typeSpecifierFactory::create - - typeSpecifierFactory: - class: PHPStan\Analyser\TypeSpecifierFactory - - relativePathHelper: - class: PHPStan\File\RelativePathHelper - factory: PHPStan\File\FuzzyRelativePathHelper - arguments: - currentWorkingDirectory: %currentWorkingDirectory% - analysedPaths: %analysedPaths% - fallbackRelativePathHelper: @parentDirectoryRelativePathHelper - - simpleRelativePathHelper: - class: PHPStan\File\RelativePathHelper - factory: PHPStan\File\SimpleRelativePathHelper - arguments: - currentWorkingDirectory: %currentWorkingDirectory% - autowired: false - - parentDirectoryRelativePathHelper: - class: PHPStan\File\ParentDirectoryRelativePathHelper - arguments: - parentDirectory: %currentWorkingDirectory% - autowired: false - - broker: - class: PHPStan\Broker\Broker - factory: @brokerFactory::create - autowired: - - PHPStan\Broker\Broker - - brokerFactory: - class: PHPStan\Broker\BrokerFactory - - cacheStorage: - class: PHPStan\Cache\FileCacheStorage - arguments: - directory: %tmpDir%/cache/PHPStan - autowired: no - - currentPhpVersionRichParser: - class: PHPStan\Parser\RichParser - arguments: - parser: @currentPhpVersionPhpParser - autowired: no - - currentPhpVersionSimpleParser: - class: PHPStan\Parser\SimpleParser - arguments: - parser: @currentPhpVersionPhpParser - autowired: no - - phpParserDecorator: - class: PHPStan\Parser\PhpParserDecorator - arguments: - wrappedParser: @PHPStan\Parser\Parser - autowired: - - PhpParser\Parser - - currentPhpVersionLexer: - class: PhpParser\Lexer - factory: @PHPStan\Parser\LexerFactory::create() - autowired: false - - currentPhpVersionPhpParser: - class: PhpParser\Parser\Php7 - arguments: - lexer: @currentPhpVersionLexer - autowired: false - - registry: - class: PHPStan\Rules\Registry - factory: @PHPStan\Rules\RegistryFactory::create - - stubPhpDocProvider: - class: PHPStan\PhpDoc\StubPhpDocProvider - arguments: - stubFiles: %stubFiles% - - # Reflection providers - - reflectionProviderFactory: - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory - arguments: - runtimeReflectionProvider: @runtimeReflectionProvider - staticReflectionProvider: @betterReflectionProvider - disableRuntimeReflectionProvider: %featureToggles.disableRuntimeReflectionProvider% - - reflectionProvider: - factory: @PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory::create - autowired: - - PHPStan\Reflection\ReflectionProvider - - betterReflectionSourceLocator: - class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator - factory: @PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory::create - autowired: false - - betterReflectionClassReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - nodeScopeResolverClassReflector: - factory: @betterReflectionClassReflector - autowired: false - - betterReflectionFunctionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector - arguments: - classReflector: @betterReflectionClassReflector - sourceLocator: @betterReflectionSourceLocator - autowired: false - - betterReflectionConstantReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector - arguments: - classReflector: @betterReflectionClassReflector - sourceLocator: @betterReflectionSourceLocator - 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% - singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - autowired: false - - innerRuntimeReflectionProvider: - class: PHPStan\Reflection\Runtime\RuntimeReflectionProvider - - - - class: PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory - arguments: - parser: @phpParserDecorator - php8Parser: @php8PhpParser - scanFiles: %scanFiles% - scanDirectories: %scanDirectories% - analysedPaths: %analysedPaths% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - analysedPathsFromConfig: %analysedPathsFromConfig% - singleReflectionFile: %singleReflectionFile% - staticReflectionClassNamePatterns: %staticReflectionClassNamePatterns% - - - - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory - - - - class: PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber - arguments: - phpParser: @php8PhpParser - phpVersionId: %phpVersion% - autowired: - - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber - - - - class: PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber - autowired: - - PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber - - php8Lexer: - class: PhpParser\Lexer\Emulative - autowired: false - - php8PhpParser: - class: PhpParser\Parser\Php7 - 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 - - # Error formatters - - errorFormatter.raw: - class: PHPStan\Command\ErrorFormatter\RawErrorFormatter - - errorFormatter.table: - class: PHPStan\Command\ErrorFormatter\TableErrorFormatter - arguments: - showTipsOfTheDay: %tipsOfTheDay% - editorUrl: %editorUrl% - - errorFormatter.checkstyle: - class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper - - errorFormatter.json: - class: PHPStan\Command\ErrorFormatter\JsonErrorFormatter - arguments: - pretty: false - - errorFormatter.junit: - class: PHPStan\Command\ErrorFormatter\JunitErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper - - errorFormatter.prettyJson: - class: PHPStan\Command\ErrorFormatter\JsonErrorFormatter - arguments: - pretty: true - - errorFormatter.gitlab: - class: PHPStan\Command\ErrorFormatter\GitlabErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper + class: PHPStan\Rules\Properties\UninitializedPropertyRule - errorFormatter.github: - class: PHPStan\Command\ErrorFormatter\GithubErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper + # autowired services are now registered with the help of attributes + # like #[PHPStan\DependencyInjection\AutowiredService] or #[PHPStan\DependencyInjection\GenerateFactory] - errorFormatter.teamcity: - class: PHPStan\Command\ErrorFormatter\TeamcityErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper + # non-autowired services are now registered in services.neon diff --git a/conf/config.stubFiles.neon b/conf/config.stubFiles.neon deleted file mode 100644 index 1c90d8cdf1..0000000000 --- a/conf/config.stubFiles.neon +++ /dev/null @@ -1,20 +0,0 @@ -parameters: - stubFiles: - - ../stubs/ReflectionAttribute.stub - - ../stubs/ReflectionClass.stub - - ../stubs/ReflectionClassConstant.stub - - ../stubs/ReflectionFunctionAbstract.stub - - ../stubs/ReflectionParameter.stub - - ../stubs/ReflectionProperty.stub - - ../stubs/iterable.stub - - ../stubs/ArrayObject.stub - - ../stubs/WeakReference.stub - - ../stubs/ext-ds.stub - - ../stubs/PDOStatement.stub - - ../stubs/date.stub - - ../stubs/zip.stub - - ../stubs/dom.stub - - ../stubs/spl.stub - - ../stubs/SplObjectStorage.stub - - ../stubs/Exception.stub - - ../stubs/arrayFunctions.stub diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index 2f8d462dec..ad90340a64 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -1,46 +1,50 @@ parameters: checkThisOnly: false checkClassCaseSensitivity: true - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingTypehints: true checkMissingCallableSignature: false + __validate: false services: - class: PHPStan\PhpDoc\StubSourceLocatorFactory arguments: php8Parser: @php8PhpParser - stubFiles: %stubFiles% - nodeScopeResolverClassReflector: - factory: @stubClassReflector + # overrides service from parsers.neon + defaultAnalysisParser!: + factory: @stubParser - stubBetterReflectionProvider: - class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider - arguments: - classReflector: @stubClassReflector - functionReflector: @stubFunctionReflector - constantReflector: @stubConstantReflector + # overrides service from services.neon + nodeScopeResolverReflector: + factory: @stubReflector + + # overrides service from services.neon + reflectionProvider: + factory: @stubBetterReflectionProvider + autowired: + - PHPStan\Reflection\ReflectionProvider + + # overrides service from parsers.neon + currentPhpVersionLexer: + factory: @php8Lexer autowired: false - stubClassReflector: - class: PHPStan\BetterReflection\Reflector\ClassReflector - arguments: - sourceLocator: @stubSourceLocator + # overrides service from parsers.neon + currentPhpVersionPhpParser: + factory: @php8PhpParser autowired: false - stubFunctionReflector: - class: PHPStan\BetterReflection\Reflector\FunctionReflector + stubBetterReflectionProvider: + class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: - classReflector: @stubClassReflector - sourceLocator: @stubSourceLocator + reflector: @stubReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - stubConstantReflector: - class: PHPStan\BetterReflection\Reflector\ConstantReflector + stubReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector arguments: - classReflector: @stubClassReflector sourceLocator: @stubSourceLocator autowired: false @@ -48,8 +52,3 @@ services: class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: @PHPStan\PhpDoc\StubSourceLocatorFactory::create() autowired: false - - reflectionProvider: - factory: @stubBetterReflectionProvider - autowired: - - PHPStan\Reflection\ReflectionProvider diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon new file mode 100644 index 0000000000..2654a3345b --- /dev/null +++ b/conf/parametersSchema.neon @@ -0,0 +1,206 @@ +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(), + checkNonStringableDynamicAccess: bool(), + checkParameterCastableToNumberFunctions: bool(), + skipCheckGenericClasses: listOf(string()), + stricterFunctionMap: bool() + reportPreciseLineForUnusedFunctionParameter: bool() + checkPrintfParameterTypes: bool() + internalTag: bool() + newStaticInAbstractClassStaticMethod: bool() + checkExtensionsForComparisonOperators: bool() + reportTooWideBool: bool() + rawMessageInBaseline: bool() + reportNestedTooWideType: 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() + checkTooWideParameterOutInProtectedAndPublicMethods: bool() + checkTooWideReturnTypesInProtectedAndPublicMethods: bool() + checkUninitializedProperties: bool() + checkDynamicProperties: bool() + strictRulesInstalled: bool() + deprecationRulesInstalled: bool() + inferPrivatePropertyTypeFromConstructor: bool() + checkStrictPrintfPlaceholderTypes: 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(80599)), + structure([ + min: schema(int(), min(70100), max(80599)), + max: schema(int(), min(70100), max(80599)) + ]) + ), 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([ + ?message: string() + ?messages: listOf(string()) + ?rawMessage: string() + ?identifier: string() + ?identifiers: listOf(string()) + ?path: string() + ?paths: listOf(string()) + ?count: int() + ?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: arrayOf(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()) + + # internal - editor mode + singleReflectionFile: schema(string(), nullable()) + singleReflectionInsteadOfFile: schema(string(), nullable()) + +expandRelativePaths: + - '[parameters][paths][]' + - '[parameters][excludePaths][]' + - '[parameters][excludePaths][analyse][]' + - '[parameters][excludePaths][analyseAndScan][]' + - '[parameters][ignoreErrors][][paths][]' + - '[parameters][ignoreErrors][][path]' + - '[parameters][bootstrapFiles][]' + - '[parameters][scanFiles][]' + - '[parameters][scanDirectories][]' + - '[parameters][tmpDir]' + - '[parameters][pro][tmpDir]' + - '[parameters][memoryLimitFile]' + - '[parameters][benchmarkFile]' + - '[parameters][stubFiles][]' + - '[parameters][symfony][consoleApplicationLoader]' + - '[parameters][symfony][containerXmlPath]' + - '[parameters][doctrine][objectManagerLoader]' diff --git a/conf/parsers.neon b/conf/parsers.neon new file mode 100644 index 0000000000..450df00487 --- /dev/null +++ b/conf/parsers.neon @@ -0,0 +1,85 @@ +services: + php8Parser: + class: PHPStan\Parser\SimpleParser + arguments: + parser: @php8PhpParser + autowired: false + + php8Lexer: + class: PhpParser\Lexer\Emulative + factory: @PHPStan\Parser\LexerFactory::createEmulative() + autowired: false + + php8PhpParser: + class: PhpParser\Parser\Php8 + arguments: + lexer: @php8Lexer + 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 + + currentPhpVersionSimpleDirectParser: + class: PHPStan\Parser\SimpleParser + arguments: + parser: @currentPhpVersionPhpParser + autowired: no + + currentPhpVersionSimpleParser: + class: PHPStan\Parser\CleaningParser + arguments: + wrappedParser: @currentPhpVersionSimpleDirectParser + autowired: no + + currentPhpVersionRichParser: + class: PHPStan\Parser\RichParser + arguments: + parser: @currentPhpVersionPhpParser + autowired: no + + pathRoutingParser: + class: PHPStan\Parser\PathRoutingParser + arguments: + currentPhpVersionRichParser: @currentPhpVersionRichParser + currentPhpVersionSimpleParser: @currentPhpVersionSimpleParser + php8Parser: @php8Parser + singleReflectionFile: %singleReflectionFile% + autowired: false + + phpParserDecorator: + class: PHPStan\Parser\PhpParserDecorator + arguments: + wrappedParser: @defaultAnalysisParser + autowired: false + + defaultAnalysisParser: + class: PHPStan\Parser\CachedParser + arguments: + originalParser: @pathRoutingParser + cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + 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 diff --git a/conf/services.neon b/conf/services.neon new file mode 100644 index 0000000000..0e1382d4b3 --- /dev/null +++ b/conf/services.neon @@ -0,0 +1,255 @@ +# these services are not registered using an attribute for one reason or another + +services: + # not registered using attributes because it's in vendor/ + - + class: PhpParser\BuilderFactory + + - + class: PhpParser\NodeVisitor\NameResolver + arguments: + options: + preserveOriginalNames: true + + - + class: PHPStan\PhpDocParser\ParserConfig + arguments: + usedAttributes: + lines: true + + - + class: PHPStan\PhpDocParser\Lexer\Lexer + + - + class: PHPStan\PhpDocParser\Parser\TypeParser + + - + class: PHPStan\PhpDocParser\Parser\ConstExprParser + + - + class: PHPStan\PhpDocParser\Parser\PhpDocParser + + - + class: PHPStan\PhpDocParser\Printer\Printer + + - + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory::create() + autowired: + - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber + + - + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory::create() + autowired: + - PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + + originalBetterReflectionReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector + arguments: + sourceLocator: @betterReflectionSourceLocator + autowired: false + + + # not registered using attributes because we don't want to apply service tags automatically + - + class: PHPStan\Dependency\ExportedNodeVisitor + + - + class: PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor + + - + class: PHPStan\Reflection\Php\PhpClassReflectionExtension + arguments: + parser: @defaultAnalysisParser + inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% + + - + class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension + + - + class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension + arguments: + classes: %universalObjectCratesClasses% + + - + class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension + arguments: + mixinExcludeClasses: %mixinExcludeClasses% + + - + class: PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension + arguments: + mixinExcludeClasses: %mixinExcludeClasses% + + - + class: PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension + + - + # checked as part of OverridingMethodRule + class: PHPStan\Rules\Methods\MethodSignatureRule + arguments: + reportMaybes: %reportMaybesInMethodSignatures% + reportStatic: %reportStaticMethodSignatures% + + phpstanDiagnoseExtension: + class: PHPStan\Diagnose\PHPStanDiagnoseExtension + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + configPhpVersion: %phpVersion% + autowired: false + + # not registered using attributes because there is 2+ instances + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClass + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClassConstant + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionFunctionAbstract + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionParameter + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionProperty + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + dateTimeClass: DateTime + + - + class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + dateTimeClass: DateTimeImmutable + + - + 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\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension + arguments: + class: PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension + arguments: + class: PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + errorFormatter.json: + class: PHPStan\Command\ErrorFormatter\JsonErrorFormatter + arguments: + pretty: false + + errorFormatter.prettyJson: + class: PHPStan\Command\ErrorFormatter\JsonErrorFormatter + arguments: + pretty: true + + stubFileTypeMapper: + class: PHPStan\Type\FileTypeMapper + arguments: + phpParser: @stubParser + autowired: false + + 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 + + # not registered using attributes because it's overriden in TestCase.neon or config.stubValidator.neon + + cacheStorage: + class: PHPStan\Cache\FileCacheStorage + arguments: + directory: %tmpDir%/cache/PHPStan + autowired: no + + betterReflectionSourceLocator: + factory: @PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory::create + autowired: false + + reflectionProvider: + factory: @PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory::create + autowired: + - PHPStan\Reflection\ReflectionProvider + + nodeScopeResolverReflector: + factory: @betterReflectionReflector + autowired: false + + # not registered using attributes because people often override it + + exceptionTypeResolver: + class: PHPStan\Rules\Exceptions\ExceptionTypeResolver + factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver 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-10483/bug-10483.php b/e2e/bug-10483/bug-10483.php new file mode 100644 index 0000000000..df3867ef78 --- /dev/null +++ b/e2e/bug-10483/bug-10483.php @@ -0,0 +1,10 @@ + + */ +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 @@ +eventDispatcher = sfContext::getInstance()->getEventDispatcher(); + } + + /** + * Calls methods defined via sfEventDispatcher. + * + * If a method cannot be found via sfEventDispatcher, the method name will + * be parsed to magically handle getMyFactory() and setMyFactory() methods. + * + * @param string $method The method name + * @param array $arguments The method arguments + * + * @return mixed The returned value of the called method + * + * @throws sfException if call fails + */ + public function __call($method, $arguments) + { + $event = $this->dispatcher->notifyUntil(new sfEvent($this, 'context.method_not_found', ['method' => $method, 'arguments' => $arguments])); + if (!$event->isProcessed()) { + $verb = substr($method, 0, 3); // get | set + $factory = strtolower(substr($method, 3)); // factory name + + if ('get' == $verb && $this->has($factory)) { + return $this->factories[$factory]; + } + if ('set' == $verb && isset($arguments[0])) { + return $this->set($factory, $arguments[0]); + } + + throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method)); + } + + return $event->getReturnValue(); + } + + /** + * Creates a new context instance. + * + * @param sfApplicationConfiguration $configuration An sfApplicationConfiguration instance + * @param string $name A name for this context (application name by default) + * @param string $class The context class to use (sfContext by default) + * + * @return sfContext An sfContext instance + * + * @throws sfFactoryException + */ + public static function createInstance(sfApplicationConfiguration $configuration, $name = null, $class = __CLASS__) + { + if (null === $name) { + $name = $configuration->getApplication(); + } + + self::$current = $name; + + self::$instances[$name] = new $class(); + + if (!self::$instances[$name] instanceof sfContext) { + throw new sfFactoryException(sprintf('Class "%s" is not of the type sfContext.', $class)); + } + + self::$instances[$name]->initialize($configuration); + + return self::$instances[$name]; + } + + /** + * Initializes the current sfContext instance. + * + * @param sfApplicationConfiguration $configuration An sfApplicationConfiguration instance + */ + public function initialize(sfApplicationConfiguration $configuration) + { + $this->configuration = $configuration; + $this->dispatcher = $configuration->getEventDispatcher(); + + try { + $this->loadFactories(); + } catch (sfException $e) { + $e->printStackTrace(); + } catch (Exception $e) { + sfException::createFromException($e)->printStackTrace(); + } + + $this->dispatcher->connect('template.filter_parameters', [$this, 'filterTemplateParameters']); + $this->dispatcher->connect('response.fastcgi_finish_request', [$this, 'shutdownUserAndStorage']); + + // register our shutdown function + register_shutdown_function([$this, 'shutdown']); + } + + /** + * Retrieves the singleton instance of this class. + * + * @param string $name the name of the sfContext to retrieve + * @param string $class The context class to use (sfContext by default) + * + * @return sfContext an sfContext implementation instance + * + * @throws sfException + */ + public static function getInstance($name = null, $class = __CLASS__) + { + if (null === $name) { + $name = self::$current; + } + + if (!isset(self::$instances[$name])) { + throw new sfException(sprintf('The "%s" context does not exist.', $name)); + } + + return self::$instances[$name]; + } + + /** + * Checks to see if there has been a context created. + * + * @param string $name The name of the sfContext to check for + * + * @return bool true is instanced, otherwise false + */ + public static function hasInstance($name = null) + { + if (null === $name) { + $name = self::$current; + } + + return isset(self::$instances[$name]); + } + + /** + * Loads the symfony factories. + */ + public function loadFactories() + { + if (sfConfig::get('sf_use_database')) { + // setup our database connections + $this->factories['databaseManager'] = new sfDatabaseManager($this->configuration, ['auto_shutdown' => false]); + } + + // create a new action stack + $this->factories['actionStack'] = new sfActionStack(); + + if (sfConfig::get('sf_debug') && sfConfig::get('sf_logging_enabled')) { + $timer = sfTimerManager::getTimer('Factories'); + } + + // include the factories configuration + require $this->configuration->getConfigCache()->checkConfig('config/factories.yml'); + + $this->dispatcher->notify(new sfEvent($this, 'context.load_factories')); + + if (sfConfig::get('sf_debug') && sfConfig::get('sf_logging_enabled')) { + // @var $timer sfTimer + $timer->addTime(); + } + } + + /** + * Dispatches the current request. + */ + public function dispatch() + { + $this->getController()->dispatch(); + } + + /** + * Sets the current context to something else. + * + * @param string $name The name of the context to switch to + */ + public static function switchTo($name) + { + if (!isset(self::$instances[$name])) { + $currentConfiguration = sfContext::getInstance()->getConfiguration(); + sfContext::createInstance(ProjectConfiguration::getApplicationConfiguration($name, $currentConfiguration->getEnvironment(), $currentConfiguration->isDebug())); + } + + self::$current = $name; + + sfContext::getInstance()->getConfiguration()->activate(); + } + + /** + * Returns the configuration instance. + * + * @return sfApplicationConfiguration The current application configuration instance + */ + public function getConfiguration() + { + return $this->configuration; + } + + /** + * Retrieves the current event dispatcher. + * + * @return sfEventDispatcher An sfEventDispatcher instance + */ + public function getEventDispatcher() + { + return $this->dispatcher; + } + + /** + * Retrieve the action name for this context. + * + * @return string|null the currently executing action name if one is set, null otherwise + */ + public function getActionName() + { + // get the last action stack entry + if ($this->factories['actionStack'] && $lastEntry = $this->factories['actionStack']->getLastEntry()) { + // @var $lastEntry sfActionStackEntry + return $lastEntry->getActionName(); + } + + return null; + } + + /** + * Retrieve the ActionStack. + * + * @return sfActionStack the sfActionStack instance + */ + public function getActionStack() + { + return $this->factories['actionStack']; + } + + /** + * Retrieve the controller. + * + * @return sfFrontWebController the current sfController implementation instance + */ + public function getController() + { + return isset($this->factories['controller']) ? $this->factories['controller'] : null; + } + + /** + * Retrieves the mailer. + * + * @return sfMailer the current sfMailer implementation instance + */ + public function getMailer() + { + if (!isset($this->factories['mailer'])) { + $this->factories['mailer'] = new $this->mailerConfiguration['class']($this->dispatcher, $this->mailerConfiguration); + } + + return $this->factories['mailer']; + } + + /** + * Set mailer configuration. + * + * @param array $configuration + */ + public function setMailerConfiguration($configuration) + { + $this->mailerConfiguration = $configuration; + } + + /** + * Retrieve the logger. + * + * @return sfLogger the current sfLogger implementation instance + */ + public function getLogger() + { + if (!isset($this->factories['logger'])) { + $this->factories['logger'] = new sfNoLogger($this->dispatcher); + } + + return $this->factories['logger']; + } + + /** + * Retrieve a database connection from the database manager. + * + * This is a shortcut to manually getting a connection from an existing + * database implementation instance. + * + * If the [sf_use_database] setting is off, this will return null. + * + * @param string $name a database name + * + * @return mixed a database instance + * + * @throws sfDatabaseException if the requested database name does not exist + */ + public function getDatabaseConnection($name = 'default') + { + if (null !== $this->factories['databaseManager']) { + return $this->factories['databaseManager']->getDatabase($name)->getConnection(); + } + + return null; + } + + /** + * Retrieve the database manager. + * + * @return sfDatabaseManager the current sfDatabaseManager instance + */ + public function getDatabaseManager() + { + return isset($this->factories['databaseManager']) ? $this->factories['databaseManager'] : null; + } + + /** + * Retrieve the module directory for this context. + * + * @return string|null an absolute filesystem path to the directory of the currently executing module if one is set, null otherwise + */ + public function getModuleDirectory() + { + // get the last action stack entry + if (isset($this->factories['actionStack']) && $lastEntry = $this->factories['actionStack']->getLastEntry()) { + // @var $lastEntry sfActionStackEntry + return sfConfig::get('sf_app_module_dir').'/'.$lastEntry->getModuleName(); + } + + return null; + } + + /** + * Retrieve the module name for this context. + * + * @return string|null the currently executing module name if one is set, null otherwise + */ + public function getModuleName() + { + // get the last action stack entry + if (isset($this->factories['actionStack']) && $lastEntry = $this->factories['actionStack']->getLastEntry()) { + // @var $lastEntry sfActionStackEntry + return $lastEntry->getModuleName(); + } + + return null; + } + + /** + * Retrieve the request. + * + * @return sfRequest the current sfRequest implementation instance + */ + public function getRequest() + { + return isset($this->factories['request']) ? $this->factories['request'] : null; + } + + /** + * Retrieve the response. + * + * @return sfResponse the current sfResponse implementation instance + */ + public function getResponse() + { + return isset($this->factories['response']) ? $this->factories['response'] : null; + } + + /** + * Set the response object. + * + * @param sfResponse $response an sfResponse instance + */ + public function setResponse($response) + { + $this->factories['response'] = $response; + } + + /** + * Retrieve the storage. + * + * @return sfStorage the current sfStorage implementation instance + */ + public function getStorage() + { + return isset($this->factories['storage']) ? $this->factories['storage'] : null; + } + + /** + * Retrieve the view cache manager. + * + * @return sfViewCacheManager the current sfViewCacheManager implementation instance + */ + public function getViewCacheManager() + { + return isset($this->factories['viewCacheManager']) ? $this->factories['viewCacheManager'] : null; + } + + /** + * Retrieve the i18n instance. + * + * @return sfI18N the current sfI18N implementation instance + * + * @throws sfConfigurationException + */ + public function getI18N() + { + if (!sfConfig::get('sf_i18n')) { + throw new sfConfigurationException('You must enable i18n support in your settings.yml configuration file.'); + } + + return $this->factories['i18n']; + } + + /** + * Retrieve the routing instance. + * + * @return sfRouting the current sfRouting implementation instance + */ + public function getRouting() + { + return isset($this->factories['routing']) ? $this->factories['routing'] : null; + } + + /** + * Retrieve the user. + * + * @return sfUser the current sfUser implementation instance + */ + public function getUser() + { + return isset($this->factories['user']) ? $this->factories['user'] : null; + } + + /** + * Retrieves the service container. + * + * @return sfServiceContainer the current sfServiceContainer implementation instance + */ + public function getServiceContainer() + { + if (!isset($this->factories['serviceContainer'])) { + $this->factories['serviceContainer'] = new $this->serviceContainerConfiguration['class'](); + $this->factories['serviceContainer']->setService('sf_event_dispatcher', $this->configuration->getEventDispatcher()); + $this->factories['serviceContainer']->setService('sf_formatter', new sfFormatter()); + $this->factories['serviceContainer']->setService('sf_user', $this->getUser()); + $this->factories['serviceContainer']->setService('sf_routing', $this->getRouting()); + } + + return $this->factories['serviceContainer']; + } + + /** + * Set service ontainer configuration. + */ + public function setServiceContainerConfiguration(array $config) + { + $this->serviceContainerConfiguration = $config; + } + + /** + * Retrieves a service from the service container. + * + * @param string $id The service identifier + * + * @return object The service instance + */ + public function getService($id) + { + return $this->getServiceContainer()->getService($id); + } + + /** + * Returns the configuration cache. + * + * @return sfConfigCache A sfConfigCache instance + */ + public function getConfigCache() + { + return $this->configuration->getConfigCache(); + } + + /** + * Returns true if the context object exists (implements the ArrayAccess interface). + * + * @param string $name The name of the context object + * + * @return bool true if the context object exists, false otherwise + */ + #[\ReturnTypeWillChange] + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the context object associated with the name (implements the ArrayAccess interface). + * + * @param string $name The offset of the value to get + * + * @return mixed The context object if exists, null otherwise + */ + #[\ReturnTypeWillChange] + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Sets the context object associated with the offset (implements the ArrayAccess interface). + * + * @param string $offset Service name + * @param mixed $value Service + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + /** + * Unsets the context object associated with the offset (implements the ArrayAccess interface). + * + * @param string $offset The parameter name + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->factories[$offset]); + } + + /** + * Gets an object from the current context. + * + * @param string $name The name of the object to retrieve + * + * @return object The object associated with the given name + */ + public function get($name) + { + if (!$this->has($name)) { + throw new sfException(sprintf('The "%s" object does not exist in the current context.', $name)); + } + + return $this->factories[$name]; + } + + /** + * Puts an object in the current context. + * + * @param string $name The name of the object to store + * @param mixed $object The object to store + */ + public function set($name, $object) + { + $this->factories[$name] = $object; + } + + /** + * Returns true if an object is currently stored in the current context with the given name, false otherwise. + * + * @param string $name The object name + * + * @return bool true if the object is not null, false otherwise + */ + public function has($name) + { + return isset($this->factories[$name]); + } + + /** + * Listens to the template.filter_parameters event. + * + * @param sfEvent $event An sfEvent instance + * @param array $parameters An array of template parameters to filter + * + * @return array The filtered parameters array + */ + public function filterTemplateParameters(sfEvent $event, $parameters) + { + $parameters['sf_context'] = $this; + $parameters['sf_request'] = $this->factories['request']; + $parameters['sf_params'] = $this->factories['request']->getParameterHolder(); + $parameters['sf_response'] = $this->factories['response']; + $parameters['sf_user'] = $this->factories['user']; + + return $parameters; + } + + /** + * Shuts the user/storage down. + * + * @internal Should be called only via invoking "response.fastcgi_finish_request" or context shutting down. + */ + public function shutdownUserAndStorage() + { + if (!$this->hasShutdownUserAndStorage && $this->has('user')) { + $this->getUser()->shutdown(); + $this->getStorage()->shutdown(); + + $this->hasShutdownUserAndStorage = true; + } + } + + /** + * Execute the shutdown procedure. + */ + public function shutdown() + { + $this->shutdownUserAndStorage(); + + if ($this->has('routing')) { + $this->getRouting()->shutdown(); + } + + if (sfConfig::get('sf_use_database')) { + $this->getDatabaseManager()->shutdown(); + } + + if (sfConfig::get('sf_logging_enabled')) { + $this->getLogger()->shutdown(); + } + } + + /** + * Extract the class or interface name from filename. + * + * @param string $filename a filename + * + * @return string a class or interface name, if one can be extracted, otherwise null + */ + public static function extractClassName($filename) + { + $retval = null; + + if (self::isPathAbsolute($filename)) { + $filename = basename($filename); + } + + $pattern = '/(.*?)\.(class|interface)\.php/i'; + + if (preg_match($pattern, $filename, $match)) { + $retval = $match[1]; + } + + return $retval; + } + + /** + * Clear all files in a given directory. + * + * @param string $directory an absolute filesystem path to a directory + */ + public static function clearDirectory($directory) + { + if (!is_dir($directory)) { + return; + } + + // open a file point to the cache dir + $fp = opendir($directory); + + // ignore names + $ignore = ['.', '..', 'CVS', '.svn']; + + while (($file = readdir($fp)) !== false) { + if (!in_array($file, $ignore)) { + if (is_link($directory.'/'.$file)) { + // delete symlink + unlink($directory.'/'.$file); + } elseif (is_dir($directory.'/'.$file)) { + // recurse through directory + self::clearDirectory($directory.'/'.$file); + + // delete the directory + rmdir($directory.'/'.$file); + } else { + // delete the file + unlink($directory.'/'.$file); + } + } + } + + // close file pointer + closedir($fp); + } + + /** + * Clear all files and directories corresponding to a glob pattern. + * + * @param string $pattern an absolute filesystem pattern + */ + public static function clearGlob($pattern) + { + if (false === $files = glob($pattern)) { + return; + } + + // order is important when removing directories + sort($files); + + foreach ($files as $file) { + if (is_dir($file)) { + // delete directory + self::clearDirectory($file); + } else { + // delete file + unlink($file); + } + } + } + + /** + * Determine if a filesystem path is absolute. + * + * @param string $path a filesystem path + * + * @return bool true, if the path is absolute, otherwise false + */ + public static function isPathAbsolute($path) + { + if ('/' == $path[0] || '\\' == $path[0] + || ( + strlen($path) > 3 && ctype_alpha($path[0]) + && ':' == $path[1] + && ('\\' == $path[2] || '/' == $path[2]) + ) + ) { + return true; + } + + return false; + } + + /** + * Strips comments from php source code. + * + * @param string $source PHP source code + * + * @return string comment free source code + */ + public static function stripComments($source) + { + if (!function_exists('token_get_all')) { + return $source; + } + + $ignore = [T_COMMENT => true, T_DOC_COMMENT => true]; + $output = ''; + + foreach (token_get_all($source) as $token) { + // array + if (isset($token[1])) { + // no action on comments + if (!isset($ignore[$token[0]])) { + // anything else -> output "as is" + $output .= $token[1]; + } + } else { + // simple 1-character token + $output .= $token; + } + } + + return $output; + } + + /** + * Strip slashes recursively from array. + * + * @param array $value the value to strip + * + * @return array clean value with slashes stripped + */ + public static function stripslashesDeep($value) + { + return is_array($value) ? array_map(['sfToolkit', 'stripslashesDeep'], $value) : stripslashes($value); + } + + // code from php at moechofe dot com (array_merge comment on php.net) + /* + * array arrayDeepMerge ( array array1 [, array array2 [, array ...]] ) + * + * Like array_merge + * + * arrayDeepMerge() merges the elements of one or more arrays together so + * that the values of one are appended to the end of the previous one. It + * returns the resulting array. + * If the input arrays have the same string keys, then the later value for + * that key will overwrite the previous one. If, however, the arrays contain + * numeric keys, the later value will not overwrite the original value, but + * will be appended. + * If only one array is given and the array is numerically indexed, the keys + * get reindexed in a continuous way. + * + * Different from array_merge + * If string keys have arrays for values, these arrays will merge recursively. + */ + public static function arrayDeepMerge() + { + switch (func_num_args()) { + case 0: + return false; + + case 1: + return func_get_arg(0); + + case 2: + $args = func_get_args(); + $args[2] = []; + if (is_array($args[0]) && is_array($args[1])) { + foreach (array_unique(array_merge(array_keys($args[0]), array_keys($args[1]))) as $key) { + $isKey0 = array_key_exists($key, $args[0]); + $isKey1 = array_key_exists($key, $args[1]); + if ($isKey0 && $isKey1 && is_array($args[0][$key]) && is_array($args[1][$key])) { + $args[2][$key] = self::arrayDeepMerge($args[0][$key], $args[1][$key]); + } elseif ($isKey0 && $isKey1) { + $args[2][$key] = $args[1][$key]; + } elseif (!$isKey1) { + $args[2][$key] = $args[0][$key]; + } elseif (!$isKey0) { + $args[2][$key] = $args[1][$key]; + } + } + + return $args[2]; + } + + return $args[1]; + + default: + $args = func_get_args(); + $args[1] = sfToolkit::arrayDeepMerge($args[0], $args[1]); + array_shift($args); + + return call_user_func_array(['sfToolkit', 'arrayDeepMerge'], $args); + + break; + } + } + + /** + * Converts string to array. + * + * @param string $string the value to convert to array + * + * @return array + */ + public static function stringToArray($string) + { + preg_match_all('/ + \s*((?:\w+-)*\w+) # key \\1 + \s*=\s* # = + (\'|")? # values may be included in \' or " \\2 + (.*?) # value \\3 + (?(2) \\2) # matching \' or " if needed \\4 + \s*(?: + (?=\w+\s*=) | \s*$ # followed by another key= or the end of the string + ) + /x', (string) $string, $matches, PREG_SET_ORDER); + + $attributes = []; + foreach ($matches as $val) { + $attributes[$val[1]] = self::literalize($val[3]); + } + + return $attributes; + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * + * @param string $value + * @param bool $quoted Quote? + */ + public static function literalize($value, $quoted = false) + { + // lowercase our value for comparison + $value = trim($value); + $lvalue = strtolower($value); + + if (in_array($lvalue, ['null', '~', ''])) { + $value = null; + } elseif (in_array($lvalue, ['true', 'on', '+', 'yes'])) { + $value = true; + } elseif (in_array($lvalue, ['false', 'off', '-', 'no'])) { + $value = false; + } elseif (ctype_digit($value)) { + $value = (int) $value; + } elseif (is_numeric($value)) { + $value = (float) $value; + } else { + $value = self::replaceConstants($value); + if ($quoted) { + $value = '\''.str_replace('\'', '\\\'', $value).'\''; + } + } + + return $value; + } + + /** + * Replaces constant identifiers in a scalar value. + * + * @param string $value the value to perform the replacement on + * + * @return string the value with substitutions made + */ + public static function replaceConstants($value) + { + if (!is_string($value)) { + return $value; + } + + return preg_replace_callback('/%(.+?)%/', function ($v) { + return sfConfig::has(strtolower($v[1])) ? sfConfig::get(strtolower($v[1])) : '%'.$v[1].'%'; + }, $value); + } + + /** + * Returns subject replaced with regular expression matchs. + * + * @param mixed $search subject to search + * @param array $replacePairs array of search => replace pairs + */ + public static function pregtr($search, $replacePairs) + { + return preg_replace(array_keys($replacePairs), array_values($replacePairs), (string) $search); + } + + /** + * Checks if array values are empty. + * + * @param array $array the array to check + * + * @return bool true if empty, otherwise false + */ + public static function isArrayValuesEmpty($array) + { + static $isEmpty = true; + foreach ($array as $value) { + $isEmpty = is_array($value) ? self::isArrayValuesEmpty($value) : '' === (string) $value; + if (!$isEmpty) { + break; + } + } + + return $isEmpty; + } + + /** + * Checks if a string is an utf8. + * + * Yi Stone Li + * Copyright (c) 2007 Yahoo! Inc. All rights reserved. + * Licensed under the BSD open source license + * + * @param string + * + * @return bool true if $string is valid UTF-8 and false otherwise + */ + public static function isUTF8($string) + { + for ($idx = 0, $strlen = strlen($string); $idx < $strlen; ++$idx) { + $byte = ord($string[$idx]); + + if ($byte & 0x80) { + if (($byte & 0xE0) == 0xC0) { + // 2 byte char + $bytes_remaining = 1; + } elseif (($byte & 0xF0) == 0xE0) { + // 3 byte char + $bytes_remaining = 2; + } elseif (($byte & 0xF8) == 0xF0) { + // 4 byte char + $bytes_remaining = 3; + } else { + return false; + } + + if ($idx + $bytes_remaining >= $strlen) { + return false; + } + + while ($bytes_remaining--) { + if ((ord($string[++$idx]) & 0xC0) != 0x80) { + return false; + } + } + } + } + + return true; + } + + /** + * Returns an array value for a path. + * + * @param array $values The values to search + * @param string $name The token name + * @param array $default Default if not found + * + * @return array + */ + public static function getArrayValueForPath($values, $name, $default = null) + { + if (false === $offset = strpos($name, '[')) { + return isset($values[$name]) ? $values[$name] : $default; + } + + if (!isset($values[substr($name, 0, $offset)])) { + return $default; + } + + $array = $values[substr($name, 0, $offset)]; + + while (false !== $pos = strpos($name, '[', $offset)) { + $end = strpos($name, ']', $pos); + if ($end == $pos + 1) { + // reached a [] + if (!is_array($array)) { + return $default; + } + + break; + } + if (!isset($array[substr($name, $pos + 1, $end - $pos - 1)])) { + return $default; + } + if (is_array($array)) { + $array = $array[substr($name, $pos + 1, $end - $pos - 1)]; + $offset = $end; + } else { + return $default; + } + } + + return $array; + } + + /** + * Get path to php cli. + * + * @return string + * + * @throws sfException If no php cli found + */ + public static function getPhpCli() + { + $path = getenv('PATH') ?: getenv('Path'); + $suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : ['.exe', '.bat', '.cmd', '.com']) : ['']; + foreach (['php5', 'php'] as $phpCli) { + foreach ($suffixes as $suffix) { + foreach (explode(PATH_SEPARATOR, $path) as $dir) { + if (is_file($file = $dir.DIRECTORY_SEPARATOR.$phpCli.$suffix) && is_executable($file)) { + return $file; + } + } + } + } + + throw new sfException('Unable to find PHP executable.'); + } + + /** + * Converts strings to UTF-8 via iconv. NB, the result may not by UTF-8 if the conversion failed. + * + * This file comes from Prado (BSD License) + * + * @param string $string string to convert to UTF-8 + * @param string $from current encoding + * + * @return string UTF-8 encoded string, original string if iconv failed + */ + public static function I18N_toUTF8($string, $from) + { + $from = strtoupper($from); + if ('UTF-8' != $from) { + $s = iconv($from, 'UTF-8', $string); // to UTF-8 + + return false !== $s ? $s : $string; // it could return false + } + + return $string; + } + + /** + * Converts UTF-8 strings to a different encoding. NB. The result may not have been encoded if iconv fails. + * + * This file comes from Prado (BSD License) + * + * @param string $string the UTF-8 string for conversion + * @param string $to new encoding + * + * @return string encoded string + */ + public static function I18N_toEncoding($string, $to) + { + $to = strtoupper($to); + if ('UTF-8' != $to) { + $s = iconv('UTF-8', $to, $string); + + return false !== $s ? $s : $string; + } + + return $string; + } + + /** + * Adds a path to the PHP include_path setting. + * + * @param mixed $path Single string path or an array of paths + * @param string $position Either 'front' or 'back' + * + * @return string The old include path + */ + public static function addIncludePath($path, $position = 'front') + { + if (is_array($path)) { + foreach ('front' == $position ? array_reverse($path) : $path as $p) { + self::addIncludePath($p, $position); + } + + return; + } + + $paths = explode(PATH_SEPARATOR, get_include_path()); + + // remove what's already in the include_path + if (false !== $key = array_search(realpath($path), array_map('realpath', $paths))) { + unset($paths[$key]); + } + + switch ($position) { + case 'front': + array_unshift($paths, $path); + + break; + + case 'back': + $paths[] = $path; + + break; + + default: + throw new InvalidArgumentException(sprintf('Unrecognized position: "%s"', $position)); + } + + return set_include_path(implode(PATH_SEPARATOR, $paths)); + } + + protected $type = 'file'; + protected $names = []; + protected $prunes = []; + protected $discards = []; + protected $execs = []; + protected $mindepth = 0; + protected $sizes = []; + protected $maxdepth = 1000000; + protected $relative = false; + protected $follow_link = false; + protected $sort = false; + protected $ignore_version_control = true; + + /** + * Sets maximum directory depth. + * + * Finder will descend at most $level levels of directories below the starting point. + * + * @param int $level + * + * @return sfFinder current sfFinder object + */ + public function maxdepth($level) + { + $this->maxdepth = $level; + + return $this; + } + + /** + * Sets minimum directory depth. + * + * Finder will start applying tests at level $level. + * + * @param int $level + * + * @return sfFinder current sfFinder object + */ + public function mindepth($level) + { + $this->mindepth = $level; + + return $this; + } + + public function get_type() + { + return $this->type; + } + + /** + * Sets the type of elements to returns. + * + * @param string $name directory or file or any (for both file and directory) + * + * @return sfFinder new sfFinder object + */ + public static function type($name) + { + $finder = new self(); + + return $finder->setType($name); + } + + /** + * Sets the type of elements to returns. + * + * @param string $name directory or file or any (for both file and directory) + * + * @return sfFinder Current object + */ + public function setType($name) + { + $name = strtolower($name); + + if ('dir' === substr($name, 0, 3)) { + $this->type = 'directory'; + + return $this; + } + if ('any' === $name) { + $this->type = 'any'; + + return $this; + } + + $this->type = 'file'; + + return $this; + } + + /** + * Adds rules that files must match. + * + * You can use patterns (delimited with / sign), globs or simple strings. + * + * $finder->name('*.php') + * $finder->name('/\.php$/') // same as above + * $finder->name('test.php') + * + * @param list a list of patterns, globs or strings + * + * @return sfFinder Current object + */ + public function name() + { + $args = func_get_args(); + $this->names = array_merge($this->names, $this->args_to_array($args)); + + return $this; + } + + /** + * Adds rules that files must not match. + * + * @see ->name() + * + * @param list a list of patterns, globs or strings + * + * @return sfFinder Current object + */ + public function not_name() + { + $args = func_get_args(); + $this->names = array_merge($this->names, $this->args_to_array($args, true)); + + return $this; + } + + /** + * Adds tests for file sizes. + * + * $finder->size('> 10K'); + * $finder->size('<= 1Ki'); + * $finder->size(4); + * + * @param list a list of comparison strings + * + * @return sfFinder Current object + */ + public function size() + { + $args = func_get_args(); + $numargs = count($args); + for ($i = 0; $i < $numargs; ++$i) { + $this->sizes[] = new sfNumberCompare($args[$i]); + } + + return $this; + } + + /** + * Traverses no further. + * + * @param list a list of patterns, globs to match + * + * @return sfFinder Current object + */ + public function prune() + { + $args = func_get_args(); + $this->prunes = array_merge($this->prunes, $this->args_to_array($args)); + + return $this; + } + + /** + * Discards elements that matches. + * + * @param list a list of patterns, globs to match + * + * @return sfFinder Current object + */ + public function discard() + { + $args = func_get_args(); + $this->discards = array_merge($this->discards, $this->args_to_array($args)); + + return $this; + } + + /** + * Ignores version control directories. + * + * Currently supports Subversion, CVS, DARCS, Gnu Arch, Monotone, Bazaar-NG, GIT, Mercurial + * + * @param bool $ignore falase when version control directories shall be included (default is true) + * + * @return sfFinder Current object + */ + public function ignore_version_control($ignore = true) + { + $this->ignore_version_control = $ignore; + + return $this; + } + + /** + * Returns files and directories ordered by name. + * + * @return sfFinder Current object + */ + public function sort_by_name() + { + $this->sort = 'name'; + + return $this; + } + + /** + * Returns files and directories ordered by type (directories before files), then by name. + * + * @return sfFinder Current object + */ + public function sort_by_type() + { + $this->sort = 'type'; + + return $this; + } + + /** + * Executes function or method for each element. + * + * Element match if functino or method returns true. + * + * $finder->exec('myfunction'); + * $finder->exec(array($object, 'mymethod')); + * + * @param mixed function or method to call + * + * @return sfFinder Current object + */ + public function exec() + { + $args = func_get_args(); + $numargs = count($args); + for ($i = 0; $i < $numargs; ++$i) { + if (is_array($args[$i]) && !method_exists($args[$i][0], $args[$i][1])) { + throw new sfException(sprintf('method "%s" does not exist for object "%s".', $args[$i][1], $args[$i][0])); + } + if (!is_array($args[$i]) && !function_exists($args[$i])) { + throw new sfException(sprintf('function "%s" does not exist.', $args[$i])); + } + + $this->execs[] = $args[$i]; + } + + return $this; + } + + /** + * Returns relative paths for all files and directories. + * + * @return sfFinder Current object + */ + public function relative() + { + $this->relative = true; + + return $this; + } + + /** + * Symlink following. + * + * @return sfFinder Current object + */ + public function follow_link() + { + $this->follow_link = true; + + return $this; + } + + /** + * Searches files and directories which match defined rules. + * + * @return array list of files and directories + */ + public function in() + { + $files = []; + $here_dir = getcwd(); + + $finder = clone $this; + + if ($this->ignore_version_control) { + $ignores = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; + + $finder->discard($ignores)->prune($ignores); + } + + // first argument is an array? + $numargs = func_num_args(); + $arg_list = func_get_args(); + if (1 === $numargs && is_array($arg_list[0])) { + $arg_list = $arg_list[0]; + $numargs = count($arg_list); + } + + for ($i = 0; $i < $numargs; ++$i) { + $dir = realpath($arg_list[$i]); + + if (!is_dir($dir)) { + continue; + } + + $dir = str_replace('\\', '/', $dir); + + // absolute path? + if (!self::isPathAbsolute($dir)) { + $dir = $here_dir.'/'.$dir; + } + + $new_files = str_replace('\\', '/', $finder->search_in($dir)); + + if ($this->relative) { + $new_files = preg_replace('#^'.preg_quote(rtrim($dir, '/'), '#').'/#', '', $new_files); + } + + $files = array_merge($files, $new_files); + } + + if ('name' === $this->sort) { + sort($files); + } + + return array_unique($files); + } + + public static function isPathAbsolute($path) + { + if ('/' === $path[0] || '\\' === $path[0] + || ( + strlen($path) > 3 && ctype_alpha($path[0]) + && ':' === $path[1] + && ('\\' === $path[2] || '/' === $path[2]) + ) + ) { + return true; + } + + return false; + } + + // glob, patterns (must be //) or strings + protected function to_regex($str) + { + if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $str)) { + return $str; + } + + return sfGlobToRegex::glob_to_regex($str); + } + + protected function args_to_array($arg_list, $not = false) + { + $list = []; + $nbArgList = count($arg_list); + for ($i = 0; $i < $nbArgList; ++$i) { + if (is_array($arg_list[$i])) { + foreach ($arg_list[$i] as $arg) { + $list[] = [$not, $this->to_regex($arg)]; + } + } else { + $list[] = [$not, $this->to_regex($arg_list[$i])]; + } + } + + return $list; + } + + protected function search_in($dir, $depth = 0) + { + if ($depth > $this->maxdepth) { + return []; + } + + $dir = realpath($dir); + + if ((!$this->follow_link) && is_link($dir)) { + return []; + } + + $files = []; + $temp_files = []; + $temp_folders = []; + if (is_dir($dir) && is_readable($dir)) { + $current_dir = opendir($dir); + while (false !== $entryname = readdir($current_dir)) { + if ('.' == $entryname || '..' == $entryname) { + continue; + } + + $current_entry = $dir.DIRECTORY_SEPARATOR.$entryname; + if ((!$this->follow_link) && is_link($current_entry)) { + continue; + } + + if (is_dir($current_entry)) { + if ('type' === $this->sort) { + $temp_folders[$entryname] = $current_entry; + } else { + if (('directory' === $this->type || 'any' === $this->type) && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname)) { + $files[] = $current_entry; + } + + if (!$this->is_pruned($dir, $entryname)) { + $files = array_merge($files, $this->search_in($current_entry, $depth + 1)); + } + } + } else { + if (('directory' !== $this->type || 'any' === $this->type) && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->size_ok($dir, $entryname) && $this->exec_ok($dir, $entryname)) { + if ('type' === $this->sort) { + $temp_files[] = $current_entry; + } else { + $files[] = $current_entry; + } + } + } + } + + if ('type' === $this->sort) { + ksort($temp_folders); + foreach ($temp_folders as $entryname => $current_entry) { + if (('directory' === $this->type || 'any' === $this->type) && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname)) { + $files[] = $current_entry; + } + + if (!$this->is_pruned($dir, $entryname)) { + $files = array_merge($files, $this->search_in($current_entry, $depth + 1)); + } + } + + sort($temp_files); + $files = array_merge($files, $temp_files); + } + + closedir($current_dir); + } + + return $files; + } + + protected function match_names($dir, $entry) + { + if (!count($this->names)) { + return true; + } + + // Flags indicating that there was attempts to match + // at least one "not_name" or "name" rule respectively + // to following variables: + $one_not_name_rule = false; + $one_name_rule = false; + + foreach ($this->names as $args) { + list($not, $regex) = $args; + $not ? $one_not_name_rule = true : $one_name_rule = true; + if (preg_match($regex, $entry)) { + // We must match ONLY ONE "not_name" or "name" rule: + // if "not_name" rule matched then we return "false" + // if "name" rule matched then we return "true" + return $not ? false : true; + } + } + + if ($one_not_name_rule && $one_name_rule) { + return false; + } + if ($one_not_name_rule) { + return true; + } + if ($one_name_rule) { + return false; + } + + return true; + } + + protected function size_ok($dir, $entry) + { + if (0 === count($this->sizes)) { + return true; + } + + if (!is_file($dir.DIRECTORY_SEPARATOR.$entry)) { + return true; + } + + $filesize = filesize($dir.DIRECTORY_SEPARATOR.$entry); + foreach ($this->sizes as $number_compare) { + if (!$number_compare->test($filesize)) { + return false; + } + } + + return true; + } + + protected function is_pruned($dir, $entry) + { + if (0 === count($this->prunes)) { + return false; + } + + foreach ($this->prunes as $args) { + $regex = $args[1]; + if (preg_match($regex, $entry)) { + return true; + } + } + + return false; + } + + protected function is_discarded($dir, $entry) + { + if (0 === count($this->discards)) { + return false; + } + + foreach ($this->discards as $args) { + $regex = $args[1]; + if (preg_match($regex, $entry)) { + return true; + } + } + + return false; + } + + protected function exec_ok($dir, $entry) + { + if (0 === count($this->execs)) { + return true; + } + + foreach ($this->execs as $exec) { + if (!call_user_func_array($exec, [$dir, $entry])) { + return false; + } + } + + return true; + } + + /** + * Executes an application defined process prior to execution of this sfAction object. + * + * By default, this method is empty. + */ + public function preExecute() + { + } + + /** + * Execute an application defined process immediately after execution of this sfAction object. + * + * By default, this method is empty. + */ + public function postExecute() + { + } + + /** + * Forwards current action to the default 404 error action. + * + * @param string $message Message of the generated exception + * + * @throws sfError404Exception + */ + public function forward404($message = null) + { + throw new sfError404Exception($this->get404Message($message)); + } + + /** + * Forwards current action to the default 404 error action unless the specified condition is true. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $message Message of the generated exception + * + * @throws sfError404Exception + */ + public function forward404Unless($condition, $message = null) + { + if (!$condition) { + throw new sfError404Exception($this->get404Message($message)); + } + } + + /** + * Forwards current action to the default 404 error action if the specified condition is true. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $message Message of the generated exception + * + * @throws sfError404Exception + */ + public function forward404If($condition, $message = null) + { + if ($condition) { + throw new sfError404Exception($this->get404Message($message)); + } + } + + /** + * Redirects current action to the default 404 error action (with browser redirection). + * + * This method stops the current code flow. + */ + public function redirect404() + { + return $this->redirect('/'.sfConfig::get('sf_error_404_module').'/'.sfConfig::get('sf_error_404_action')); + } + + /** + * Forwards current action to a new one (without browser redirection). + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param string $module A module name + * @param string $action An action name + * + * @throws sfStopException + */ + public function forward($module, $action) + { + if (sfConfig::get('sf_logging_enabled')) { + $this->dispatcher->notify(new sfEvent($this, 'application.log', [sprintf('Forward to action "%s/%s"', $module, $action)])); + } + + $this->getController()->forward($module, $action); + + throw new sfStopException(); + } + + /** + * If the condition is true, forwards current action to a new one (without browser redirection). + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $module A module name + * @param string $action An action name + * + * @throws sfStopException + */ + public function forwardIf($condition, $module, $action) + { + if ($condition) { + $this->forward($module, $action); + } + } + + /** + * Unless the condition is true, forwards current action to a new one (without browser redirection). + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $module A module name + * @param string $action An action name + * + * @throws sfStopException + */ + public function forwardUnless($condition, $module, $action) + { + if (!$condition) { + $this->forward($module, $action); + } + } + + /** + * Redirects current request to a new URL. + * + * 2 URL formats are accepted : + * - a full URL: http://www.google.com/ + * - an internal URL (url_for() format): module/action + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param string $url Url + * @param int $statusCode Status code (default to 302) + * + * @throws sfStopException + */ + public function redirect($url, $statusCode = 302) + { + // compatibility with url_for2() style signature + if (is_object($statusCode) || is_array($statusCode)) { + $url = array_merge(['sf_route' => $url], is_object($statusCode) ? ['sf_subject' => $statusCode] : $statusCode); + $statusCode = func_num_args() >= 3 ? func_get_arg(2) : 302; + } + + $this->getController()->redirect($url, 0, $statusCode); + + throw new sfStopException(); + } + + /** + * Redirects current request to a new URL, only if specified condition is true. + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $url Url + * @param int $statusCode Status code (default to 302) + * + * @throws sfStopException + * + * @see redirect + */ + public function redirectIf($condition, $url, $statusCode = 302) + { + if ($condition) { + // compatibility with url_for2() style signature + $arguments = func_get_args(); + call_user_func_array([$this, 'redirect'], array_slice($arguments, 1)); + } + } + + /** + * Redirects current request to a new URL, unless specified condition is true. + * + * This method stops the action. So, no code is executed after a call to this method. + * + * @param bool $condition A condition that evaluates to true or false + * @param string $url Url + * @param int $statusCode Status code (default to 302) + * + * @throws sfStopException + * + * @see redirect + */ + public function redirectUnless($condition, $url, $statusCode = 302) + { + if (!$condition) { + // compatibility with url_for2() style signature + $arguments = func_get_args(); + call_user_func_array([$this, 'redirect'], array_slice($arguments, 1)); + } + } + + /** + * Appends the given text to the response content and bypasses the built-in view system. + * + * This method must be called as with a return: + * + * return $this->renderText('some text') + * + * @param string $text Text to append to the response + * + * @return string sfView::NONE + */ + public function renderText($text) + { + $this->getResponse()->setContent($this->getResponse()->getContent().$text); + + return sfView::NONE; + } + + /** + * Convert the given data into a JSON response. + * + * return $this->renderJson(array('username' => 'john')) + * + * @param mixed $data Data to encode as JSON + * + * @return string sfView::NONE + */ + public function renderJson($data) + { + $this->getResponse()->setContentType('application/json'); + $this->getResponse()->setContent(json_encode($data)); + + return sfView::NONE; + } + + /** + * Returns the partial rendered content. + * + * If the vars parameter is omitted, the action's internal variables + * will be passed, just as it would to a normal template. + * + * If the vars parameter is set then only those values are + * available in the partial. + * + * @param string $templateName partial name + * @param array $vars vars + * + * @return string The partial content + */ + public function getPartial($templateName, $vars = null) + { + $this->getContext()->getConfiguration()->loadHelpers('Partial'); + + $vars = null !== $vars ? $vars : $this->varHolder->getAll(); + + return get_partial($templateName, $vars); + } + + /** + * Appends the result of the given partial execution to the response content. + * + * This method must be called as with a return: + * + * return $this->renderPartial('foo/bar') + * + * @param string $templateName partial name + * @param array $vars vars + * + * @return string sfView::NONE + * + * @see getPartial + */ + public function renderPartial($templateName, $vars = null) + { + return $this->renderText($this->getPartial($templateName, $vars)); + } + + /** + * Returns the component rendered content. + * + * If the vars parameter is omitted, the action's internal variables + * will be passed, just as it would to a normal template. + * + * If the vars parameter is set then only those values are + * available in the component. + * + * @param string $moduleName module name + * @param string $componentName component name + * @param array $vars vars + * + * @return string The component rendered content + */ + public function getComponent($moduleName, $componentName, $vars = null) + { + $this->getContext()->getConfiguration()->loadHelpers('Partial'); + + $vars = null !== $vars ? $vars : $this->varHolder->getAll(); + + return get_component($moduleName, $componentName, $vars); + } + + /** + * Appends the result of the given component execution to the response content. + * + * This method must be called as with a return: + * + * return $this->renderComponent('foo', 'bar') + * + * @param string $moduleName module name + * @param string $componentName component name + * @param array $vars vars + * + * @return string sfView::NONE + * + * @see getComponent + */ + public function renderComponent($moduleName, $componentName, $vars = null) + { + return $this->renderText($this->getComponent($moduleName, $componentName, $vars)); + } + + /** + * Returns the security configuration for this module. + * + * @return string Current security configuration as an array + */ + public function getSecurityConfiguration() + { + return $this->security; + } + + /** + * Overrides the current security configuration for this module. + * + * @param array $security The new security configuration + */ + public function setSecurityConfiguration($security) + { + $this->security = $security; + } + + /** + * Returns a value from security.yml. + * + * @param string $name The name of the value to pull from security.yml + * @param mixed $default The default value to return if none is found in security.yml + */ + public function getSecurityValue($name, $default = null) + { + $actionName = strtolower($this->getActionName()); + + if (isset($this->security[$actionName][$name])) { + return $this->security[$actionName][$name]; + } + + if (isset($this->security['all'][$name])) { + return $this->security['all'][$name]; + } + + return $default; + } + + /** + * Indicates that this action requires security. + * + * @return bool true, if this action requires security, otherwise false + */ + public function isSecure() + { + return $this->getSecurityValue('is_secure', false); + } + + /** + * Gets credentials the user must have to access this action. + * + * @return mixed An array or a string describing the credentials the user must have to access this action + */ + public function getCredential() + { + return $this->getSecurityValue('credentials'); + } + + /** + * Sets an alternate template for this sfAction. + * + * See 'Naming Conventions' in the 'Symfony View' documentation. + * + * @param string $name Template name + * @param string $module The module (current if null) + */ + public function setTemplate($name, $module = null) + { + if (sfConfig::get('sf_logging_enabled')) { + $this->dispatcher->notify(new sfEvent($this, 'application.log', [sprintf('Change template to "%s/%s"', null === $module ? 'CURRENT' : $module, $name)])); + } + + if (null !== $module) { + $dir = $this->context->getConfiguration()->getTemplateDir($module, $name.sfView::SUCCESS.'.php'); + $name = $dir.'/'.$name; + } + + sfConfig::set('symfony.view.'.$this->getModuleName().'_'.$this->getActionName().'_template', $name); + } + + /** + * Gets the name of the alternate template for this sfAction. + * + * WARNING: It only returns the template you set with the setTemplate() method, + * and does not return the template that you configured in your view.yml. + * + * See 'Naming Conventions' in the 'Symfony View' documentation. + * + * @return string Template name. Returns null if no template has been set within the action + */ + public function getTemplate() + { + return sfConfig::get('symfony.view.'.$this->getModuleName().'_'.$this->getActionName().'_template'); + } + + /** + * Sets an alternate layout for this sfAction. + * + * To de-activate the layout, set the layout name to false. + * + * To revert the layout to the one configured in the view.yml, set the template name to null. + * + * @param mixed $name Layout name or false to de-activate the layout + */ + public function setLayout($name) + { + if (sfConfig::get('sf_logging_enabled')) { + $this->dispatcher->notify(new sfEvent($this, 'application.log', [sprintf('Change layout to "%s"', $name)])); + } + + sfConfig::set('symfony.view.'.$this->getModuleName().'_'.$this->getActionName().'_layout', $name); + } + + /** + * Gets the name of the alternate layout for this sfAction. + * + * WARNING: It only returns the layout you set with the setLayout() method, + * and does not return the layout that you configured in your view.yml. + * + * @return mixed Layout name. Returns null if no layout has been set within the action + */ + public function getLayout() + { + return sfConfig::get('symfony.view.'.$this->getModuleName().'_'.$this->getActionName().'_layout'); + } + + /** + * Changes the default view class used for rendering the template associated with the current action. + * + * @param string $class View class name + */ + public function setViewClass($class) + { + sfConfig::set('mod_'.strtolower($this->getModuleName()).'_view_class', $class); + } + + /** + * Returns the current route for this request. + * + * @return sfRoute The route for the request + */ + public function getRoute() + { + return $this->getRequest()->getAttribute('sf_route'); + } + + /** + * Returns a formatted message for a 404 error. + * + * @param string $message An error message (null by default) + * + * @return string The error message or a default one if null + */ + protected function get404Message($message = null) + { + return null === $message ? sprintf('This request has been forwarded to a 404 error page by the action "%s/%s".', $this->getModuleName(), $this->getActionName()) : $message; + } +} diff --git a/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2019_09/MobileServiceApi.php b/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2019_09/MobileServiceApi.php new file mode 100644 index 0000000000..0ee3650ba8 --- /dev/null +++ b/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2019_09/MobileServiceApi.php @@ -0,0 +1,93 @@ +accessTokenService = sfContext::getInstance()->getContainer()->get(AccessToken::class, ContainerInterface::NULL_ON_INVALID_REFERENCE); + } + + +} diff --git a/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2024_3/MobileServiceApi.php b/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2024_3/MobileServiceApi.php new file mode 100644 index 0000000000..92655820dd --- /dev/null +++ b/e2e/bug13425/src/Bundle/TICKeosMobileBundle/Service/V2024_3/MobileServiceApi.php @@ -0,0 +1,88 @@ +', 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..9ed998185b --- /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, 5>', 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..9ed998185b --- /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, 5>', 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..3cae7a0628 --- /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/editor-mode/differentFoo.php b/e2e/editor-mode/differentFoo.php new file mode 100644 index 0000000000..f6908fca5f --- /dev/null +++ b/e2e/editor-mode/differentFoo.php @@ -0,0 +1,13 @@ +requireString($foo->doFoo()); + } + + public function requireString(string $s): void + { + + } + +} diff --git a/e2e/editor-mode/src/Foo.php b/e2e/editor-mode/src/Foo.php new file mode 100644 index 0000000000..a91890871f --- /dev/null +++ b/e2e/editor-mode/src/Foo.php @@ -0,0 +1,13 @@ + + */ +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 @@ +doFooTrait(); + } + +} diff --git a/e2e/result-cache-traits/src/ClassUsingBarTrait.php b/e2e/result-cache-traits/src/ClassUsingBarTrait.php new file mode 100644 index 0000000000..e8063edef4 --- /dev/null +++ b/e2e/result-cache-traits/src/ClassUsingBarTrait.php @@ -0,0 +1,10 @@ +=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.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "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.10.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": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "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.3.0" + }, + "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": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "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.44 || ^9.6.25" + }, + "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.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/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "knplabs/github-api", + "version": "v3.16.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "25d7bafd6b0dd088d4850aef7fcc74dc4fba8b28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/25d7bafd6b0dd088d4850aef7fcc74dc4fba8b28", + "reference": "25d7bafd6b0dd088d4850aef7fcc74dc4fba8b28", + "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.15-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.16.0" + }, + "funding": [ + { + "url": "https://github.com/acrobat", + "type": "github" + } + ], + "time": "2024-11-07T19:35:30+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "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/process": "^5.4 | ^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 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-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": "2025-07-20T12:47:49+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.4", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/3411aa86b104e2d5b7e760da4600865ead963c3c", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "8.0 - 8.4" + }, + "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.4" + }, + "time": "2024-10-04T22:00:08+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.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/cache-plugin.git", + "reference": "5c591e9e04602cec12307e3e1be3abefeb005e29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/5c591e9e04602cec12307e3e1be3abefeb005e29", + "reference": "5c591e9e04602cec12307e3e1be3abefeb005e29", + "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.1" + }, + "time": "2024-10-02T11:25:38+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.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "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.20.0" + }, + "time": "2024-10-02T11:20:13+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.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "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.2" + }, + "time": "2024-10-02T11:34:13+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.1.29", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phar-composer-source.git", + "reference": "git" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e", + "reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e", + "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": "2025-09-25T06:58:18+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.7", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.29" + }, + "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.7" + }, + "time": "2025-09-26T11:19: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/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.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "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.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-22T10:21:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "73089124388c8510efb8d2d1689285d285937b08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", + "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.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "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.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-05T10:16:07+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "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.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "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.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "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": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "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.6.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": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "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.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-25T06:35:40+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.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "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.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "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.x-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.6.1" + }, + "time": "2025-08-13T20:13:15+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.29", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "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.4", + "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.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "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.29" + }, + "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-09-24T06:29:11+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.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "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.9" + }, + "funding": [ + { + "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/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+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.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "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.8" + }, + "funding": [ + { + "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/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "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.8" + }, + "funding": [ + { + "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/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+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.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "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.6" + }, + "funding": [ + { + "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/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57: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": {}, + "platform-overrides": { + "php": "8.3.99" + }, + "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..a252e3bac8 --- /dev/null +++ b/issue-bot/playground.neon @@ -0,0 +1,14 @@ +rules: + - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\MethodNeverRule + - PHPStan\Rules\Playground\NotAnalysedTraitRule + - PHPStan\Rules\Playground\NoPhpCodeRule + - PHPStan\Rules\Playground\PhpdocCommentRule + +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..f0985151df --- /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()) { // @phpstan-ignore while.condNotBoolean + 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..3edaa7c1e3 --- /dev/null +++ b/issue-bot/src/Console/DownloadCommand.php @@ -0,0 +1,261 @@ +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, 80500] 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; + } + if (count($phpVersionHashes) === 0) { + continue; + } + $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..cc417f6b34 --- /dev/null +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -0,0 +1,325 @@ +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'], + ]; + } + } + + $exitCode = self::EXIT_AFFECTS_ISSUES; + if (count($toPost) === 0) { + $exitCode = self::EXIT_NO_AFFECTED_ISSUES; + $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 self::EXIT_ERROR; + } + 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 $exitCode; + } + + /** + * @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 index 73926b901a..4409109b36 100644 --- a/patches/Consistency.patch +++ b/patches/Consistency.patch @@ -1,5 +1,3 @@ -@package hoa/consistency - --- Consistency.php 2017-05-02 14:18:12.000000000 +0200 +++ Consistency.php 2020-05-05 08:28:35.000000000 +0200 @@ -319,42 +319,6 @@ diff --git a/patches/DependencyChecker.patch b/patches/DependencyChecker.patch new file mode 100644 index 0000000000..4902922537 --- /dev/null +++ b/patches/DependencyChecker.patch @@ -0,0 +1,13 @@ +--- src/DI/DependencyChecker.php 2023-10-02 21:58:38 ++++ src/DI/DependencyChecker.php 2024-07-07 09:24:35 +@@ -147,7 +147,9 @@ + $flip = array_flip($classes); + foreach ($functions as $name) { + if (strpos($name, '::')) { +- $method = new ReflectionMethod($name); ++ $method = PHP_VERSION_ID < 80300 ++ ? new ReflectionMethod($name) ++ : ReflectionMethod::createFromMethodName($name); + $class = $method->getDeclaringClass(); + if (isset($flip[$class->name])) { + continue; diff --git a/patches/File.patch b/patches/File.patch new file mode 100644 index 0000000000..0732eb0e55 --- /dev/null +++ b/patches/File.patch @@ -0,0 +1,11 @@ +--- File.php 2017-07-11 09:42:15 ++++ File.php 2024-08-26 23:13:27 +@@ -192,7 +192,7 @@ + * @throws \Hoa\File\Exception\FileDoesNotExist + * @throws \Hoa\File\Exception + */ +- protected function &_open($streamName, Stream\Context $context = null) ++ protected function &_open($streamName, ?Stream\Context $context = null) + { + if (substr($streamName, 0, 4) == 'file' && + false === is_dir(dirname($streamName))) { diff --git a/patches/HoaException.patch b/patches/HoaException.patch new file mode 100644 index 0000000000..bada616504 --- /dev/null +++ b/patches/HoaException.patch @@ -0,0 +1,11 @@ +--- Exception/Exception.php 2024-06-24 15:17:26 ++++ Exception/Exception.php 2024-06-24 15:17:51 +@@ -37,7 +37,7 @@ + namespace Hoa\Compiler\Exception; + + use Hoa\Consistency; +-use Hoa\Exception as HoaException; ++use Hoa\Exception\Exception as HoaException; + + /** + * Class \Hoa\Compiler\Exception. diff --git a/patches/HoaParser.patch b/patches/HoaParser.patch new file mode 100644 index 0000000000..46829378e2 --- /dev/null +++ b/patches/HoaParser.patch @@ -0,0 +1,11 @@ +--- Llk/Parser.php 2017-08-08 09:44:07 ++++ Llk/Parser.php 2025-09-08 12:20:08 +@@ -169,7 +169,7 @@ + $this->_trace = []; + $this->_todo = []; + +- if (false === array_key_exists($rule, $this->_rules)) { ++ if (false === array_key_exists($rule ?? '', $this->_rules)) { + $rule = $this->getRootRule(); + } + 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/OutputFormatter.patch b/patches/OutputFormatter.patch new file mode 100644 index 0000000000..e956d7210c --- /dev/null +++ b/patches/OutputFormatter.patch @@ -0,0 +1,84 @@ +--- Formatter/OutputFormatter.php ++++ Formatter/OutputFormatter.php +@@ -12,6 +12,7 @@ + namespace Symfony\Component\Console\Formatter; + + use Symfony\Component\Console\Exception\InvalidArgumentException; ++use Symfony\Component\Console\Helper\Helper; + + use function Symfony\Component\String\b; + +@@ -160,9 +161,11 @@ class OutputFormatter implements WrappableOutputFormatterInterface + continue; + } + ++ // convert byte position to character position. ++ $pos = Helper::length(substr($message, 0, $pos)); + // add the text up to the next tag +- $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); +- $offset = $pos + \strlen($text); ++ $output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); ++ $offset = $pos + Helper::length($text); + + // opening tag? + if ($open = '/' != $text[1]) { +@@ -183,7 +186,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface + } + } + +- $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); ++ $output .= $this->applyCurrentStyle(Helper::substr($message, $offset), $output, $width, $currentLineLength); + + return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); + } +@@ -253,8 +256,20 @@ class OutputFormatter implements WrappableOutputFormatterInterface + } + + if ($currentLineLength) { +- $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; +- $text = substr($text, $i); ++ $lines = explode("\n", $text, 2); ++ $prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n"; ++ $text = Helper::substr($lines[0], $i); ++ ++ if (isset($lines[1])) { ++ // $prefix may contain the full first line in which the \n is already a part of $prefix. ++ if ('' !== $text) { ++ $text .= "\n"; ++ } ++ ++ $text .= $lines[1]; ++ } ++ ++ unset($lines); + } else { + $prefix = ''; + } +@@ -269,8 +284,8 @@ class OutputFormatter implements WrappableOutputFormatterInterface + + $lines = explode("\n", $text); + +- foreach ($lines as $line) { +- $currentLineLength += \strlen($line); ++ foreach ($lines as $i => $line) { ++ $currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line); + if ($width <= $currentLineLength) { + $currentLineLength = 0; + } +--- Helper/Helper.php ++++ Helper/Helper.php +@@ -100,6 +100,14 @@ abstract class Helper implements HelperInterface + { + $string ?? $string = ''; + ++ if (preg_match('//u', $string)) { ++ $result = grapheme_substr((new UnicodeString($string))->toString(), $from, $length); ++ ++ return false === $result ++ ? '' ++ : $result; ++ } ++ + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return substr($string, $from, $length); + } 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/Resolver.patch b/patches/Resolver.patch new file mode 100644 index 0000000000..c743addc1b --- /dev/null +++ b/patches/Resolver.patch @@ -0,0 +1,27 @@ +--- src/DI/Resolver.php 2023-10-02 21:58:38 ++++ src/DI/Resolver.php 2025-08-26 09:38:08 +@@ -61,13 +61,13 @@ + + public function resolveDefinition(Definition $def): void + { +- if ($this->recursive->contains($def)) { ++ if (isset($this->recursive[$def])) { + $names = array_map(function ($item) { return $item->getName(); }, iterator_to_array($this->recursive)); + throw new ServiceCreationException(sprintf('Circular reference detected for services: %s.', implode(', ', $names))); + } + + try { +- $this->recursive->attach($def); ++ $this->recursive[$def] = true; + + $def->resolveType($this); + +@@ -78,7 +78,7 @@ + throw $this->completeException($e, $def); + + } finally { +- $this->recursive->detach($def); ++ unset($this->recursive[$def]); + } + } + 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/SessionHandler.patch b/patches/SessionHandler.patch index 5b05a02df5..ba45ffc1b5 100644 --- a/patches/SessionHandler.patch +++ b/patches/SessionHandler.patch @@ -1,23 +1,20 @@ -@package jetbrains/phpstorm-stubs -@version dev-master - ---- session/SessionHandler.php 2021-09-09 18:57:58.000000000 +0200 -+++ session/SessionHandler.php 2021-09-12 19:00:13.000000000 +0200 -@@ -137,7 +137,7 @@ - * Note this value is returned internally to PHP for processing. +--- 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 @@ *

*/ -- public function validateId(string $id); -+ public function validateId($id); + #[TentativeType] +- public function validateId(string $id): bool; ++ public function validateId($id): bool; /** * Update timestamp of a session -@@ -152,7 +152,7 @@ - *

+@@ -163,7 +163,7 @@ * @return bool */ -- public function updateTimestamp(string $id, string $data); -+ public function updateTimestamp($id, $data); + #[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 index 5abd708df1..8dbb2e108e 100644 --- a/patches/Stream.patch +++ b/patches/Stream.patch @@ -1,7 +1,5 @@ -@package hoa/stream - ---- Stream.php 2017-02-21 17:01:06.000000000 +0100 -+++ Stream.php 2021-04-19 17:10:20.000000000 +0200 +--- Stream.php 2024-08-26 23:05:49 ++++ Stream.php 2024-08-26 23:01:08 @@ -192,7 +192,7 @@ * @return array * @throws \Hoa\Stream\Exception @@ -11,6 +9,15 @@ $streamName, Stream $handler, $context = null +@@ -250,7 +250,7 @@ + * @return resource + * @throws \Hoa\Exception\Exception + */ +- abstract protected function &_open($streamName, Context $context = null); ++ abstract protected function &_open($streamName, ?Context $context = null); + + /** + * Close the current stream. @@ -687,11 +687,6 @@ Consistency::flexEntity('Hoa\Stream\Stream'); diff --git a/patches/TreeNode.patch b/patches/TreeNode.patch new file mode 100644 index 0000000000..39e7917def --- /dev/null +++ b/patches/TreeNode.patch @@ -0,0 +1,14 @@ +--- Llk/TreeNode.php 2017-08-08 09:44:07 ++++ Llk/TreeNode.php 2024-08-26 23:07:29 +@@ -95,9 +95,9 @@ + */ + public function __construct( + $id, +- array $value = null, ++ ?array $value = null, + array $children = [], +- self $parent = null ++ ?self $parent = null + ) { + $this->setId($id); + diff --git a/patches/Wrapper.patch b/patches/Wrapper.patch index ade4d0a2fe..b8282376bd 100644 --- a/patches/Wrapper.patch +++ b/patches/Wrapper.patch @@ -1,5 +1,3 @@ -@package hoa/protocol - --- Wrapper.php 2017-01-14 13:26:10.000000000 +0100 +++ Wrapper.php 2020-05-05 08:39:18.000000000 +0200 @@ -582,24 +582,3 @@ diff --git a/patches/cloudflare-ca.patch b/patches/cloudflare-ca.patch new file mode 100644 index 0000000000..57f6cf5a77 --- /dev/null +++ b/patches/cloudflare-ca.patch @@ -0,0 +1,27 @@ +--- ca-bundle/res/cacert.pem 2024-06-18 13:50:00 ++++ ca-bundle/res/cacert.pem 2024-06-18 13:50:29 +@@ -3579,3 +3579,24 @@ + HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 + o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= + -----END CERTIFICATE----- ++ ++Cloudflare CA ++================================== ++-----BEGIN CERTIFICATE----- ++MIIC6zCCAkygAwIBAgIUI7b68p0pPrCBoW4ptlyvVcPItscwCgYIKoZIzj0EAwQw ++gY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T ++YW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9DbG91ZGZsYXJlLCBJbmMxNzA1BgNVBAMT ++LkNsb3VkZmxhcmUgZm9yIFRlYW1zIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw ++HhcNMjAwMjA0MTYwNTAwWhcNMjUwMjAyMTYwNTAwWjCBjTELMAkGA1UEBhMCVVMx ++EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAW ++BgNVBAoTD0Nsb3VkZmxhcmUsIEluYzE3MDUGA1UEAxMuQ2xvdWRmbGFyZSBmb3Ig ++VGVhbXMgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0eTCBmzAQBgcqhkjOPQIBBgUr ++gQQAIwOBhgAEAVdXsX8tpA9NAQeEQalvUIcVaFNDvGsR69ysZxOraRWNGHLfq1mi ++P6o3wtmtx/C2OXG01Cw7UFJbKl5MEDxnT2KoAdFSynSJOF2NDoe5LoZHbUW+yR3X ++FDl+MF6JzZ590VLGo6dPBf06UsXbH7PvHH2XKtFt8bBXVNMa5a21RdmpD0Pho0Uw ++QzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU ++YBcQng1AEMMNteuRDAMG0/vgFe0wCgYIKoZIzj0EAwQDgYwAMIGIAkIBQU5OTA2h ++YqmFk8paan5ezHVLcmcucsfYw4L/wmeEjCkczRmCVNm6L86LjhWU0v0wER0e+lHO ++3efvjbsu8gIGSagCQgEBnyYMP9gwg8l96QnQ1khFA1ljFlnqc2XgJHDSaAJC0gdz +++NV3JMeWaD2Rb32jc9r6/a7xY0u0ByqxBQ1OQ0dt7A== ++-----END CERTIFICATE----- diff --git a/patches/dom_c.patch b/patches/dom_c.patch new file mode 100644 index 0000000000..9e542c826d --- /dev/null +++ b/patches/dom_c.patch @@ -0,0 +1,17 @@ +--- dom/dom_c.php 2024-01-02 12:04:54 ++++ dom/dom_c.php 2024-01-21 10:41:56 +@@ -1347,6 +1347,14 @@ + */ + class DOMNamedNodeMap implements IteratorAggregate, Countable + { ++ ++ /** ++ * The number of nodes in the map. The range of valid child node indices is 0 to length - 1 inclusive. ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $length; + /** + * Retrieves a node specified by name + * @link https://php.net/manual/en/domnamednodemap.getnameditem.php diff --git a/patches/paratest.patch b/patches/paratest.patch new file mode 100644 index 0000000000..f2091828ff --- /dev/null +++ b/patches/paratest.patch @@ -0,0 +1,35 @@ +--- src/Runners/PHPUnit/Worker/BaseWorker.php 2020-02-07 23:07:07.000000000 +0100 ++++ src/Runners/PHPUnit/Worker/BaseWorker.php 2022-03-27 17:35:45.000000000 +0200 +@@ -28,17 +28,18 @@ + array $parameters = [], + ?Options $options = null + ) { +- $bin = 'PARATEST=1 '; ++ $env = getenv(); ++ $env['PARATEST'] = 1; + if (\is_numeric($token)) { +- $bin .= 'XDEBUG_CONFIG="true" '; +- $bin .= "TEST_TOKEN=$token "; ++ $env['XDEBUG_CONFIG'] = 'true'; ++ $env['TEST_TOKEN'] = $token; + } + if ($uniqueToken) { +- $bin .= "UNIQUE_TEST_TOKEN=$uniqueToken "; ++ $env['UNIQUE_TEST_TOKEN'] = $uniqueToken; + } + $finder = new PhpExecutableFinder(); + $phpExecutable = $finder->find(); +- $bin .= "$phpExecutable "; ++ $bin = "$phpExecutable "; + if ($options && $options->passthruPhp) { + $bin .= $options->passthruPhp . ' '; + } +@@ -50,7 +51,7 @@ + if ($options && $options->verbose) { + echo "Starting WrapperWorker via: $bin\n"; + } +- $process = \proc_open($bin, self::$descriptorspec, $pipes); ++ $process = \proc_open($bin, self::$descriptorspec, $pipes, null, $env); + $this->proc = \is_resource($process) ? $process : null; + $this->pipes = $pipes; + } diff --git a/patches/xmlreader.patch b/patches/xmlreader.patch new file mode 100644 index 0000000000..7be168133f --- /dev/null +++ b/patches/xmlreader.patch @@ -0,0 +1,122 @@ +--- xmlreader/xmlreader.php 2024-01-21 10:44:31 ++++ xmlreader/xmlreader.php 2024-01-21 10:48:24 +@@ -28,7 +28,119 @@ + */ + class XMLReader + { ++ /** ++ * The number of attributes on the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $attributeCount; ++ ++ /** ++ * The base URI of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $baseURI; ++ ++ /** ++ * Depth of the node in the tree, starting at 0 ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $depth; ++ ++ /** ++ * Indicates if node has attributes ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasAttributes; ++ ++ /** ++ * Indicates if node has a text value ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasValue; ++ ++ /** ++ * Indicates if attribute is defaulted from DTD ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isDefault; ++ ++ /** ++ * Indicates if node is an empty element tag ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isEmptyElement; ++ ++ /** ++ * The local name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $localName; ++ + /** ++ * The qualified name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $name; ++ ++ /** ++ * The URI of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $namespaceURI; ++ ++ /** ++ * The node type for the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $nodeType; ++ ++ /** ++ * The prefix of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $prefix; ++ ++ /** ++ * The text value of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $value; ++ ++ /** ++ * The xml:lang scope which the node resides ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $xmlLang; ++ ++ /** + * No node type + */ + public const NONE = 0; diff --git a/phpcs.xml b/phpcs.xml index b3b5bb5651..0945cbbce6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,54 +1,54 @@ + + - + 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 + + + - + - src/Command/CommandHelper.php @@ -57,56 +57,75 @@ tests - - src/Command/AnalyseApplication.php + + + + + + + + + + + + + - - - - + + src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - - + - + - - - - - - - - - - - - - - - + - - - + - - src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php + + + + - - src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php + + + + + + + + + + + + + + + + + + + + + + + + src/Type/TypeResult.php - tests/*/data + compiler/tests/*/data/ + tests/*/Fixture/ + tests/*/cache/ + tests/vendor/* + 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 ac6123fc51..1ec2cd321b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,351 +1,1966 @@ parameters: ignoreErrors: - - message: "#^Only numeric types are allowed in pre\\-decrement, bool\\|float\\|int\\|string\\|null given\\.$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType count: 1 + path: build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php + + - + rawMessage: '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 + + - + rawMessage: Cannot assign offset 'realCount' to array|string. + identifier: offsetAssign.dimType + count: 1 + path: src/Analyser/Ignore/IgnoredErrorHelperResult.php + + - + rawMessage: Casting to string something that's already string. + identifier: cast.useless + count: 3 path: src/Analyser/MutatingScope.php - - message: "#^Only numeric types are allowed in pre\\-increment, bool\\|float\\|int\\|string\\|null given\\.$#" + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Analyser/MutatingScope.php + + - + rawMessage: '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\\.$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: 'Call to method __construct() of internal class PhpParser\Internal\TokenStream from outside its root namespace PhpParser.' + identifier: method.internalClass + count: 1 + path: src/Analyser/RuleErrorTransformer.php + + - + rawMessage: Instantiation of internal class PhpParser\Internal\TokenStream. + identifier: new.internalClass + count: 1 + path: src/Analyser/RuleErrorTransformer.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Anonymous function has an unused use $container. + identifier: closure.unusedUse + count: 1 + path: src/Command/CommandHelper.php + + - + rawMessage: '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 + + - + rawMessage: 'Parameter #1 $path of function dirname expects string, string|false given.' + identifier: argument.type count: 1 path: src/Command/CommandHelper.php - - message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" + rawMessage: 'Static property PHPStan\Command\CommandHelper::$reservedMemory is never read, only written.' + identifier: property.onlyWritten count: 1 path: src/Command/CommandHelper.php - - message: "#^Parameter \\#1 \\$headers \\(array\\\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$headers \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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\\(\\)$#" + rawMessage: '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\\(\\)$#" + rawMessage: '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\\(\\)$#" + rawMessage: 'Parameter #2 $rows (array>) of method PHPStan\Command\ErrorsConsoleStyle::table() should be contravariant with parameter $rows (array) of method Symfony\Component\Console\Style\SymfonyStyle::table()' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" + rawMessage: 'Call to static method escape() of internal class Nette\DI\Helpers from outside its root namespace Nette.' + identifier: staticMethod.internalClass + count: 1 + path: src/DependencyInjection/AutowiredAttributeServicesExtension.php + + - + rawMessage: '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/FixerApplication.php + path: src/DependencyInjection/AutowiredAttributeServicesExtension.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" + rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.' + identifier: staticMethod.internalClass count: 1 - path: src/Command/FixerApplication.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Parameter \\#1 \\$arg of function escapeshellarg expects string, string\\|false given\\.$#" + rawMessage: '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 + + - + rawMessage: Variable method call on Nette\Schema\Elements\AnyOf|Nette\Schema\Elements\Structure|Nette\Schema\Elements\Type. + identifier: method.dynamicName count: 1 - path: src/Command/FixerApplication.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" + rawMessage: Variable static method call on Nette\Schema\Expect. + identifier: staticMethod.dynamicName count: 1 - path: src/Command/WorkerCommand.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + rawMessage: Fetching class constant PREVENT_MERGING of deprecated class Nette\DI\Config\Helpers. + identifier: classConstant.deprecatedClass count: 1 path: src/DependencyInjection/NeonAdapter.php - - message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" + rawMessage: 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 static method call on Nette\\\\Schema\\\\Expect\\.$#" + rawMessage: '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 + + - + rawMessage: 'Call to method getContent() of internal class PhpMerge\internal\Line from outside its root namespace PhpMerge.' + identifier: method.internalClass + count: 2 + path: src/Fixable/Patcher.php - - message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#" + rawMessage: 'Call to static method createArray() of internal class PhpMerge\internal\Hunk from outside its root namespace PhpMerge.' + identifier: staticMethod.internalClass count: 2 - path: src/PhpDoc/PhpDocBlock.php + path: src/Fixable/Patcher.php + + - + rawMessage: 'Call to static method createArray() of internal class PhpMerge\internal\Line from outside its root namespace PhpMerge.' + identifier: staticMethod.internalClass + count: 5 + path: src/Fixable/Patcher.php + + - + rawMessage: 'Call to method getTokenCode() of internal class PhpParser\Internal\TokenStream from outside its root namespace PhpParser.' + identifier: method.internalClass + count: 1 + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php + + - + rawMessage: 'Parameter $origTokens of method PHPStan\Fixable\PhpPrinterIndentationDetectorVisitor::__construct() has typehint with internal class PhpParser\Internal\TokenStream.' + identifier: parameter.internalClass + count: 1 + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php + + - + rawMessage: Property $origTokens references internal class PhpParser\Internal\TokenStream in its type. + identifier: property.internalClass + count: 1 + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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\\.$#" + rawMessage: '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: "#^Method PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock\\:\\:getNameScope\\(\\) should return PHPStan\\\\Analyser\\\\NameScope but returns PHPStan\\\\Analyser\\\\NameScope\\|null\\.$#" + rawMessage: '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/PhpDocNodeResolver.php + + - + rawMessage: 'Method PHPStan\PhpDoc\ResolvedPhpDocBlock::getNameScope() should return PHPStan\Analyser\NameScope but returns PHPStan\Analyser\NameScope|null.' + identifier: return.type count: 1 path: src/PhpDoc/ResolvedPhpDocBlock.php - - message: - """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ + rawMessage: 'Doing instanceof PHPStan\Type\ArrayType is error-prone and deprecated. Use Type::isArray() or Type::getArrays() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/StubValidator.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/ParamTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/ReturnTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/VarTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" + rawMessage: '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 + + - + rawMessage: Dead catch - PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName is never thrown in the try block. + identifier: catch.neverThrown + count: 4 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#" + rawMessage: Dead catch - PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode is never thrown in the try block. + identifier: catch.neverThrown count: 1 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" + rawMessage: 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: "#^Only booleans are allowed in a negated boolean, int\\|false given\\.$#" + rawMessage: 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 + + - + rawMessage: '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/AutoloadSourceLocator.php - - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" + rawMessage: '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 + + - + rawMessage: '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: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:__construct\\(\\) has parameter \\$reflection with generic class ReflectionClass but does not specify its types\\: T$#" + rawMessage: '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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" + rawMessage: ''' + Call to deprecated method isSubclassOf() of class PHPStan\Reflection\ClassReflection: + Use isSubclassOfClass instead. + ''' + identifier: method.deprecated count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getNativeReflection\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Reflection/ClassReflection.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.' + identifier: phpstanApi.instanceofType count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Property PHPStan\\\\Reflection\\\\ClassReflection\\:\\:\\$reflection with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: '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$#" + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: PHPDoc tag @var with type float|int is not subtype of native type int. + identifier: varTag.nativeType + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + rawMessage: PHPDoc tag @var with type float|int is not subtype of type int. + identifier: varTag.type + count: 4 + path: src/Reflection/InitializerExprTypeResolver.php + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: 'Function class_implements() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/Php/BuiltinMethodReflection.php + path: src/Rules/DirectRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:__construct\\(\\) has parameter \\$declaringClass with generic class ReflectionClass but does not specify its types\\: T$#" + rawMessage: 'Function class_parents() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/Php/FakeBuiltinMethodReflection.php + path: src/Rules/DirectRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: '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/Reflection/Php/FakeBuiltinMethodReflection.php + path: src/Rules/DirectRegistry.php - - message: "#^Property PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:\\$declaringClass with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: 'Property PHPStan\Rules\DirectRegistry::$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/DirectRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\NativeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: 'Property PHPStan\Rules\DirectRegistry::$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/DirectRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\PhpClassReflectionExtension\\:\\:collectTraits\\(\\) has parameter \\$class with generic class ReflectionClass but does not specify its types\\: T$#" + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/Generics/GenericAncestorsCheck.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\PhpClassReflectionExtension\\:\\:collectTraits\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/Generics/TemplateTypeCheck.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: '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/Api/ApiClassExtendsRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: '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/Api/ApiClassImplementsRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: '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/Api/ApiInstantiationRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: 'Property PHPStan\Rules\LazyRegistry::$cache with generic interface PHPStan\Rules\Rule does not specify its types: TNodeType' + identifier: missingType.generics count: 1 - path: src/Rules/Api/ApiInterfaceExtendsRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: 'Property PHPStan\Rules\LazyRegistry::$rules with generic interface PHPStan\Rules\Rule does not specify its types: TNodeType' + identifier: missingType.generics count: 1 - path: src/Rules/Api/ApiMethodCallRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: 'Doing instanceof PHPStan\Type\ArrayType is error-prone and deprecated. Use Type::isArray() or Type::getArrays() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiStaticCallRule.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiTraitUseRule.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Binary operation \"\\+\" between array\\(class\\-string\\\\) and array\\\\|false results in an error\\.$#" + rawMessage: '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: "#^Method PHPStan\\\\Rules\\\\Registry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + rawMessage: '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: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$cache with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + rawMessage: 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: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$rules with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + rawMessage: Access to an undefined property T of PHPStan\Rules\RuleError::$tip. + identifier: property.notFound + count: 2 + path: src/Rules/RuleErrorBuilder.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/RuleLevelHelper.php + + - + rawMessage: 'Call to function method_exists() with ''PHPUnit\\Framework\\TestCase'' and ''assertFileDoesNotEx…'' will always evaluate to true.' + identifier: function.alreadyNarrowedType + count: 1 + path: src/Testing/LevelsTestCase.php + + - + rawMessage: Catching internal class PHPUnit\Framework\AssertionFailedError. + identifier: catch.internalClass + count: 2 + path: src/Testing/LevelsTestCase.php + + - + rawMessage: 'Return type of method PHPStan\Testing\LevelsTestCase::compareFiles() has typehint with internal class PHPUnit\Framework\AssertionFailedError.' + identifier: return.internalClass count: 1 - path: src/Rules/Registry.php + path: src/Testing/LevelsTestCase.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + rawMessage: Anonymous function has an unused use $container. + identifier: closure.unusedUse count: 1 path: src/Testing/PHPStanTestCase.php - - message: - """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: src/Type/ObjectType.php + path: src/Type/Accessory/AccessoryArrayListType.php - - message: - """ - #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Inject %%universalObjectCratesClasses%% parameter instead\\.$# - """ + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: src/Type/ObjectType.php + path: src/Type/Accessory/AccessoryLiteralStringType.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnalyserTest.php + path: src/Type/Accessory/AccessoryLowercaseStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRule.php + path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Type/Accessory/AccessoryNonFalsyStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderRule.php + path: src/Type/Accessory/AccessoryNumericStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.php + path: src/Type/Accessory/AccessoryNumericStringType.php - - message: "#^Method PHPStan\\\\Analyser\\\\EvaluationOrderTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + rawMessage: 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/AccessoryUppercaseStringType.php - - message: - """ - #^Call to deprecated method getClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasMethodType.php - - message: - """ - #^Call to deprecated method getFunction\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasOffsetType.php + + - + rawMessage: '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: - """ - #^Call to deprecated method hasClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasOffsetValueType.php - - message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Command/AnalyseCommandTest.php + path: src/Type/Accessory/HasPropertyType.php - - message: "#^Class PHPStan\\\\Node\\\\FileNodeTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Type/Accessory/NonEmptyArrayType.php - - message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Type/Accessory/OversizedArrayType.php + + - + rawMessage: '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 + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ArrayType.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\BooleanType is error-prone and deprecated. Use Type::isBoolean() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/BooleanType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/CallableType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ClosureType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' + identifier: phpstanApi.instanceofType + count: 6 + path: src/Type/Constant/ConstantArrayType.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Constant/ConstantArrayType.php + + - + rawMessage: '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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: PHPDoc tag @var with type float|int is not subtype of type int. + identifier: varTag.type + count: 1 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: PHPDoc tag @var with type int|string is not subtype of type string. + identifier: varTag.type + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/FileTypeMapper.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/FloatType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericClassStringType.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/GenericObjectType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericObjectType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateArrayType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateBenevolentUnionType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateBooleanType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantArrayType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantIntegerType.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantStringType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateFloatType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateGenericObjectType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIntegerType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIntersectionType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIterableType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateKeyOfType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/TemplateMixedType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateNullType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectShapeType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectWithoutClassType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/TemplateStrictMixedType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateStringType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\NullType is error-prone and deprecated. Use Type::isNull() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateUnionType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\IntegerType is error-prone and deprecated. Use Type::isInteger() instead.' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/IntegerRangeType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntegerRangeType.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IntegerType is error-prone and deprecated. Use Type::isInteger() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IntegerType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\BooleanType is error-prone and deprecated. Use Type::isBoolean() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntersectionType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/IntersectionType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IterableType.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IterableType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\NullType is error-prone and deprecated. Use Type::isNull() instead.' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/NullType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectShapeType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectWithoutClassType is error-prone and deprecated. Use Type::isObject() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectShapeType.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectWithoutClassType is error-prone and deprecated. Use Type::isObject() instead.' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/ObjectType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectWithoutClassType is error-prone and deprecated. Use Type::isObject() instead.' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/ObjectWithoutClassType.php + + - + rawMessage: 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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectWithoutClassType is error-prone and deprecated. Use Type::isObject() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/StaticType.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\StringType is error-prone and deprecated. Use Type::isString() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/StringType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypeCombinator.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType + count: 8 + path: src/Type/TypeCombinator.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\NullType is error-prone and deprecated. Use Type::isNull() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + + - + rawMessage: '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 + + - + rawMessage: 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 + + - + rawMessage: Result of || is always true. + identifier: booleanOr.alwaysTrue + count: 1 + path: src/Type/TypeCombinator.php + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypeUtils.php + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypehintHelper.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypehintHelper.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionType.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/UnionType.php + + - + rawMessage: 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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: 'Doing instanceof PHPStan\Type\IntegerType is error-prone and deprecated. Use Type::isInteger() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\NullType is error-prone and deprecated. Use Type::isNull() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\VoidType is error-prone and deprecated. Use Type::isVoid() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/VoidType.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found. + identifier: constant.notFound + count: 1 + path: tests/PHPStan/Command/AnalyseCommandTest.php + + - + rawMessage: '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 + + - + rawMessage: '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 + + - + rawMessage: Access to constant on internal class InternalAnnotations\InternalFoo. + identifier: classConstant.internalClass + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + rawMessage: Access to constant on internal interface InternalAnnotations\InternalFooInterface. + identifier: classConstant.internalInterface + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + rawMessage: Access to constant on internal trait InternalAnnotations\InternalFooTrait. + identifier: classConstant.internalTrait + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: 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 + + - + rawMessage: Access to constant on internal class PHPUnit\Framework\AssertionFailedError. + identifier: classConstant.internalClass + count: 2 + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php + + - + rawMessage: Catching internal class PHPUnit\Framework\AssertionFailedError. + identifier: catch.internalClass + count: 1 + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php + + - + rawMessage: 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 - - - - - - exec - - - + + + + + + + tests/PHPStan + tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + + + + exec + levels + + + diff --git a/resources/RegexGrammar.pp b/resources/RegexGrammar.pp new file mode 100644 index 0000000000..3f49912a36 --- /dev/null +++ b/resources/RegexGrammar.pp @@ -0,0 +1,224 @@ +// +// Hoa +// +// +// @license +// +// New BSD License +// +// Copyright © 2007-2017, Hoa community. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the Hoa nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Grammar \Hoa\Regex\Grammar. +// +// Provide grammar of PCRE (Perl Compatible Regular Expression)for the LL(k) +// parser. More informations at http://pcre.org/pcre.txt, sections pcrepattern & +// pcresyntax. +// +// @copyright Copyright © 2007-2017 Hoa community. +// @license New BSD License +// + +// Character classes. +// tokens suffixed with "fc_" are the same as without such suffix but followed by "class:_class" +%token negative_class_fc_ \[\^(?=\]) -> class_fc +%token class_fc_ \[(?=\]) -> class_fc +%token class_fc:_class \] -> class +%token negative_class_ \[\^ -> class +%token class_ \[ -> class +%token class:posix_class \[:\^?[a-z]+:\] +%token class:class_ \[ +%token class:_class \] -> default +%token class:range \- +// taken over from literals but class:character has \b support on top (backspace in character classes) +%token class:character \\([aefnrtb]|c[\x00-\x7f]) +%token class:dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +%token class:character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token class:literal \\.|.|\n + +// Internal options. +// See https://www.regular-expressions.info/refmodifiers.html +// and https://www.php.net/manual/en/regexp.reference.internal-options.php +%token internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+\) + +// Lookahead and lookbehind assertions. +%token lookahead_ \(\?= +%token negative_lookahead_ \(\?! +%token lookbehind_ \(\?<= +%token negative_lookbehind_ \(\? nc +%token absolute_reference_ \(\?\((?=\d) -> c +%token relative_reference_ \(\?\((?=[\+\-]) -> c +%token c:index [\+\-]?\d+ -> default +%token assertion_reference_ \(\?\( + +// Comments. +%token comment_ \(\?# -> co +%token co:_comment \) -> default +%token co:comment .*?(?=(? mark +%token mark:name [^)]+ +%token mark:_marker \) -> default + +// Capturing group. +%token named_capturing_ \(\?P?< -> nc +%token nc:_named_capturing > -> default +%token nc:capturing_name .+?(?=(?) +%token non_capturing_ \(\?: +%token non_capturing_internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+: +%token non_capturing_reset_ \(\?\| +%token atomic_group_ \(\?> +%token capturing_ \( +%token _capturing \) + +// Quantifiers (by default, greedy). +%token zero_or_one_possessive \?\+ +%token zero_or_one_lazy \?\? +%token zero_or_one \? +%token zero_or_more_possessive \*\+ +%token zero_or_more_lazy \*\? +%token zero_or_more \* +%token one_or_more_possessive \+\+ +%token one_or_more_lazy \+\? +%token one_or_more \+ +%token exactly_n \{[0-9]+\} +%token n_to_m_possessive \{[0-9]+,[0-9]+\}\+ +%token n_to_m_lazy \{[0-9]+,[0-9]+\}\? +%token n_to_m \{[0-9]+,[0-9]+\} +%token n_or_more_possessive \{[0-9]+,\}\+ +%token n_or_more_lazy \{[0-9]+,\}\? +%token n_or_more \{[0-9]+,\} + +// Alternation. +%token alternation \| + +// Literal. +%token character \\([aefnrt]|c[\x00-\x7f]) +%token dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +// Please, see PCRESYNTAX(3), General Category properties, PCRE special category +// properties and script names for \p{} and \P{}. +%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token anchor \\([bBAZzG])|\^|\$ +%token match_point_reset \\K +%token literal \\.|.|\n + + +// Rules. + +#expression: + alternation() + +alternation: + concatenation()? ( concatenation()? #alternation )* + +concatenation: + ( internal_options() | assertion() | quantification() | condition() ) + ( ( internal_options() | assertion() | quantification() | condition() ) #concatenation )* + +#internal_options: + + +#condition: + ( + ::named_reference_:: ::_named_capturing:: #namedcondition + | ( + ::relative_reference_:: #relativecondition + | ::absolute_reference_:: #absolutecondition + ) + + | ::assertion_reference_:: alternation() #assertioncondition + ) + ::_capturing:: + alternation() + ::_capturing:: + +assertion: + ( + ::lookahead_:: #lookahead + | ::negative_lookahead_:: #negativelookahead + | ::lookbehind_:: #lookbehind + | ::negative_lookbehind_:: #negativelookbehind + ) + alternation() + ::_capturing:: + +quantification: + ( class() | simple() ) ( quantifier() #quantification )? + +quantifier: + | | + | | | + | | | + | + | | | + | | | + +#class: + ( + ::negative_class_fc_:: #negativeclass + <_class> + | ::class_fc_:: + <_class> + | ::negative_class_:: #negativeclass + | ::class_:: + ) + ? ( | | range() ? | literal() )* ? + ::_class:: + +#range: + literal() ::range:: literal() + +simple: + capturing() + | literal() + +#capturing: + ::marker_:: ::_marker:: #mark + | ::comment_:: ? ::_comment:: #comment + | ( + ::named_capturing_:: ::_named_capturing:: #namedcapturing + | ::non_capturing_:: #noncapturing + | non_capturing_internal_options() #noncapturing + | ::non_capturing_reset_:: #noncapturingreset + | ::atomic_group_:: #atomicgroup + | ::capturing_:: + ) + alternation() + ::_capturing:: + +non_capturing_internal_options: + + +literal: + + | + | + | + | + | diff --git a/resources/functionMap.php b/resources/functionMap.php index a0169e3562..c9867cf61d 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -57,10 +57,7 @@ return [ '_' => ['string', 'message'=>'string'], -'__halt_compiler' => ['void'], -'abs' => ['0|positive-int', 'number'=>'int'], -'abs\'1' => ['float', 'number'=>'float'], -'abs\'2' => ['float|0|positive-int', 'number'=>'string'], +'abs' => ['float|0|positive-int', 'num'=>'int|float'], 'accelerator_get_configuration' => ['array'], 'accelerator_get_scripts' => ['array'], 'accelerator_get_status' => ['array', 'fetch_scripts'=>'bool'], @@ -70,105 +67,31 @@ '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::getConnection' => ['AMQPConnection'], -'AMQPChannel::getPrefetchCount' => ['int'], -'AMQPChannel::getPrefetchSize' => ['int'], -'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'], -'AMQPConnection::__construct' => ['void', 'credentials='=>'array'], -'AMQPConnection::connect' => ['bool'], -'AMQPConnection::disconnect' => ['bool'], -'AMQPConnection::getHost' => ['string'], -'AMQPConnection::getLogin' => ['string'], -'AMQPConnection::getMaxChannels' => ['int|null'], -'AMQPConnection::getPassword' => ['string'], -'AMQPConnection::getPort' => ['int'], -'AMQPConnection::getReadTimeout' => ['float'], -'AMQPConnection::getTimeout' => ['float'], -'AMQPConnection::getUsedChannels' => ['int'], -'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'], -'AMQPEnvelope::getBody' => ['string'], -'AMQPEnvelope::getContentEncoding' => ['string'], -'AMQPEnvelope::getContentType' => ['string'], -'AMQPEnvelope::getCorrelationId' => ['string'], -'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::getRoutingKey' => ['string'], -'AMQPEnvelope::getTimeStamp' => ['string'], -'AMQPEnvelope::getType' => ['string'], -'AMQPEnvelope::getUserId' => ['string'], -'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::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'], -'AMQPQueue::declareQueue' => ['int'], -'AMQPQueue::delete' => ['int', 'flags='=>'int'], -'AMQPQueue::get' => ['AMQPEnvelope|bool', 'flags='=>'int'], -'AMQPQueue::getArgument' => ['bool|int|string', 'key'=>'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'], +'AMQPChannel::getChannelId' => ['int<1, 65535>'], +'AMQPChannel::getPrefetchCount' => ['int<0, 65535>'], +'AMQPChannel::getPrefetchSize' => ['int<0, max>'], +'AMQPConnection::getMaxChannels' => ['int<1, 65535>'], +'AMQPConnection::getPort' => ['int<1, 65535>'], +'AMQPConnection::getUsedChannels' => ['int<1, 65535>'], +'AMQPEnvelope::getHeaders' => ['array'], +'AMQPEnvelope::getPriority' => ['int<0, max>'], +'AMQPExchange::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPExchange::getArguments' => ['array'], +'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::setName' => ['void', 'exchangeName'=>'string|null'], +'AMQPExchange::setType' => ['void', 'exchangeType'=>'string|null'], +'AMQPExchange::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'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::delete' => ['int', 'flags='=>'int|null'], +'AMQPQueue::nack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::reject' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'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'], @@ -212,18 +135,18 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array', 'limited='=>'bool'], +'apcu_cache_info' => ['__benevolent|false>', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], 'apcu_delete' => ['bool', 'key'=>'string|APCuIterator'], -'apcu_delete\'1' => ['array', 'key'=>'string[]'], +'apcu_delete\'1' => ['list', 'key'=>'string[]'], 'apcu_entry' => ['mixed', 'key'=>'string', 'generator'=>'callable', 'ttl='=>'int'], 'apcu_exists' => ['bool', 'keys'=>'string'], 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array', 'limited='=>'bool'], +'apcu_sma_info' => ['__benevolent', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], @@ -250,7 +173,7 @@ 'apd_set_session_trace' => ['void', 'debug_level'=>'int', 'dump_directory='=>'string'], 'apd_set_session_trace_socket' => ['bool', 'tcp_server'=>'string', 'socket_type'=>'int', 'port'=>'int', 'debug_level'=>'int'], 'AppendIterator::__construct' => ['void'], -'AppendIterator::append' => ['void', 'iterator'=>'iterator'], +'AppendIterator::append' => ['void', 'iterator'=>'Iterator'], 'AppendIterator::current' => ['mixed'], 'AppendIterator::getArrayIterator' => ['ArrayIterator'], 'AppendIterator::getInnerIterator' => ['iterator'], @@ -260,10 +183,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' => ['array<0|positive-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'], @@ -283,13 +206,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 +226,7 @@ 'array_search' => ['int|string|false', 'needle'=>'mixed', 'haystack'=>'array', 'strict='=>'bool'], 'array_shift' => ['mixed', '&rw_stack'=>'array'], 'array_slice' => ['array', 'input'=>'array', 'offset'=>'int', 'length='=>'?int', 'preserve_keys='=>'bool'], -'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'array|string'], +'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'mixed'], 'array_sum' => ['int|float', 'input'=>'array'], 'array_udiff' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'], 'array_udiff\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], @@ -318,8 +241,8 @@ 'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'], -'array_unshift' => ['int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], -'array_values' => ['array', 'input'=>'array'], +'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'], @@ -350,7 +273,7 @@ 'ArrayIterator::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'ArrayIterator::unserialize' => ['void', 'serialized'=>'string'], 'ArrayIterator::valid' => ['bool'], -'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'string'], +'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'class-string'], 'ArrayObject::append' => ['void', 'value'=>'mixed'], 'ArrayObject::asort' => ['void'], 'ArrayObject::count' => ['0|positive-int'], @@ -396,7 +319,7 @@ 'BadFunctionCallException::getLine' => ['int'], 'BadFunctionCallException::getMessage' => ['string'], 'BadFunctionCallException::getPrevious' => ['(?Throwable)|(?BadFunctionCallException)'], -'BadFunctionCallException::getTrace' => ['array'], +'BadFunctionCallException::getTrace' => ['list\',args?:list,object?:object}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], 'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?BadMethodCallException)'], @@ -406,9 +329,10 @@ 'BadMethodCallException::getLine' => ['int'], 'BadMethodCallException::getMessage' => ['string'], 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], -'BadMethodCallException::getTrace' => ['array'], +'BadMethodCallException::getTrace' => ['list\',args?:list,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 +343,11 @@ 'bbcode_parse' => ['string', 'bbcode_container'=>'resource', 'to_parse'=>'string'], 'bbcode_set_arg_parser' => ['bool', 'bbcode_container'=>'resource', 'bbcode_arg_parser'=>'resource'], 'bbcode_set_flags' => ['bool', 'bbcode_container'=>'resource', 'flags'=>'int', 'mode='=>'int'], -'bcadd' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bccomp' => ['int', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcdiv' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmod' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmul' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], +'bcadd' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bccomp' => ['0|1|-1', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcdiv' => ['numeric-string|null', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcmod' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcmul' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bcompiler_load' => ['bool', 'filename'=>'string'], 'bcompiler_load_exe' => ['bool', 'filename'=>'string'], 'bcompiler_parse_class' => ['bool', 'class'=>'string', 'callback'=>'string'], @@ -437,11 +361,11 @@ 'bcompiler_write_functions_from_file' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'], 'bcompiler_write_header' => ['bool', 'filehandle'=>'resource', 'write_ver='=>'string'], 'bcompiler_write_included_filename' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'], -'bcpow' => ['numeric-string', 'base'=>'string', 'exponent'=>'string', 'scale='=>'int'], -'bcpowmod' => ['numeric-string|null', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], +'bcpow' => ['numeric-string', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'scale='=>'int'], +'bcpowmod' => ['numeric-string|null', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'modulus'=>'string', 'scale='=>'int'], 'bcscale' => ['int', 'scale='=>'int'], -'bcsqrt' => ['numeric-string', 'operand'=>'string', 'scale='=>'int'], -'bcsub' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], +'bcsqrt' => ['numeric-string', 'operand'=>'numeric-string', 'scale='=>'int'], +'bcsub' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bin2hex' => ['string', 'data'=>'string'], 'bind_textdomain_codeset' => ['string|false', 'domain'=>'string', 'codeset'=>'string'], 'bindec' => ['float|int', 'binary_number'=>'string'], @@ -484,7 +408,7 @@ 'bson_encode' => ['string', 'anything'=>'mixed'], 'bzclose' => ['bool', 'bz'=>'resource'], 'bzcompress' => ['string|int', 'source'=>'string', 'blocksize100k='=>'int', 'workfactor='=>'int'], -'bzdecompress' => ['string|false', 'source'=>'string', 'small='=>'int'], +'bzdecompress' => ['string|int|false', 'source'=>'string', 'small='=>'int'], 'bzerrno' => ['int', 'bz'=>'resource'], 'bzerror' => ['array', 'bz'=>'resource'], 'bzerrstr' => ['string', 'bz'=>'resource'], @@ -492,7 +416,7 @@ 'bzopen' => ['resource|false', 'file'=>'string|resource', 'mode'=>'string'], 'bzread' => ['string|false', 'bz'=>'resource', 'length='=>'int'], 'bzwrite' => ['int|false', 'bz'=>'resource', 'data'=>'string', 'length='=>'int'], -'CachingIterator::__construct' => ['void', 'iterator'=>'iterator', 'flags='=>''], +'CachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags='=>''], 'CachingIterator::__toString' => ['string'], 'CachingIterator::count' => ['0|positive-int'], 'CachingIterator::current' => ['mixed'], @@ -925,15 +849,15 @@ 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'call_user_method' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'parameter='=>'mixed', '...args='=>'mixed'], 'call_user_method_array' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'params'=>'array'], -'CallbackFilterIterator::__construct' => ['void', 'iterator'=>'iterator', 'func'=>'callable'], +'CallbackFilterIterator::__construct' => ['void', 'iterator'=>'Iterator', 'func'=>'callable'], 'CallbackFilterIterator::accept' => ['bool'], 'CallbackFilterIterator::current' => ['mixed'], -'CallbackFilterIterator::getInnerIterator' => ['iterator'], +'CallbackFilterIterator::getInnerIterator' => ['Iterator'], 'CallbackFilterIterator::key' => ['mixed'], 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float', 'number'=>'float'], +'ceil' => ['__benevolent', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -946,12 +870,12 @@ 'chown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], 'chr' => ['non-empty-string', 'ascii'=>'int'], 'chroot' => ['bool', 'directory'=>'string'], -'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'int', 'ending='=>'string'], +'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], -'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'class_parents' => ['array|false', 'instance'=>'object|string', 'autoload='=>'bool'], -'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'classkit_import' => ['array', 'filename'=>'string'], 'classkit_method_add' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'args'=>'string', 'code'=>'string', 'flags='=>'int'], 'classkit_method_copy' => ['bool', 'dclass'=>'string', 'dmethod'=>'string', 'sclass'=>'string', 'smethod='=>'string'], @@ -990,14 +914,14 @@ 'ClosedGeneratorException::getLine' => ['int'], 'ClosedGeneratorException::getMessage' => ['string'], 'ClosedGeneratorException::getPrevious' => ['Throwable|ClosedGeneratorException|null'], -'ClosedGeneratorException::getTrace' => ['array'], +'ClosedGeneratorException::getTrace' => ['list\',args?:list,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'], @@ -1008,7 +932,7 @@ 'Collator::__construct' => ['void', 'locale'=>'string'], 'Collator::asort' => ['bool', '&rw_arr'=>'array', 'sort_flag='=>'int'], 'Collator::compare' => ['int|false', 'str1'=>'string', 'str2'=>'string'], -'Collator::create' => ['Collator', 'locale'=>'string'], +'Collator::create' => ['?Collator', 'locale'=>'string'], 'Collator::getAttribute' => ['int', 'attr'=>'int'], 'Collator::getErrorCode' => ['int'], 'Collator::getErrorMessage' => ['string'], @@ -1021,7 +945,7 @@ 'Collator::sortWithSortKeys' => ['bool', '&rw_arr'=>'array'], 'collator_asort' => ['bool', 'coll'=>'collator', '&rw_arr'=>'array', 'sort_flag='=>'int'], 'collator_compare' => ['int|false', 'coll'=>'collator', 'str1'=>'string', 'str2'=>'string'], -'collator_create' => ['Collator', 'locale'=>'string'], +'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'coll'=>'collator', 'attr'=>'int'], 'collator_get_error_code' => ['int|false', 'coll'=>'collator'], 'collator_get_error_message' => ['string|false', 'coll'=>'collator'], @@ -1060,8 +984,8 @@ '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'], @@ -1070,303 +994,8 @@ 'copy' => ['bool', 'source_file'=>'string', 'destination_file'=>'string', 'context='=>'resource'], 'cos' => ['float', 'number'=>'float'], 'cosh' => ['float', 'number'=>'float'], -'Couchbase\AnalyticsQuery::__construct' => ['void'], -'Couchbase\AnalyticsQuery::fromString' => ['Couchbase\AnalyticsQuery', 'statement'=>'string'], -'Couchbase\basicDecoderV1' => ['mixed', 'bytes'=>'string', 'flags'=>'int', 'datatype'=>'int', 'options'=>'array'], -'Couchbase\basicEncoderV1' => ['array', 'value'=>'mixed', 'options'=>'array'], -'Couchbase\BooleanFieldSearchQuery::__construct' => ['void'], -'Couchbase\BooleanFieldSearchQuery::boost' => ['Couchbase\BooleanFieldSearchQuery', 'boost'=>'float'], -'Couchbase\BooleanFieldSearchQuery::field' => ['Couchbase\BooleanFieldSearchQuery', 'field'=>'string'], -'Couchbase\BooleanFieldSearchQuery::jsonSerialize' => ['array'], -'Couchbase\BooleanSearchQuery::__construct' => ['void'], -'Couchbase\BooleanSearchQuery::boost' => ['Couchbase\BooleanSearchQuery', 'boost'=>'float'], -'Couchbase\BooleanSearchQuery::jsonSerialize' => ['array'], -'Couchbase\BooleanSearchQuery::must' => ['Couchbase\BooleanSearchQuery', '...queries='=>'array'], -'Couchbase\BooleanSearchQuery::mustNot' => ['Couchbase\BooleanSearchQuery', '...queries='=>'array'], -'Couchbase\BooleanSearchQuery::should' => ['Couchbase\BooleanSearchQuery', '...queries='=>'array'], -'Couchbase\Bucket::__construct' => ['void'], -'Couchbase\Bucket::__get' => ['int', 'name'=>'string'], -'Couchbase\Bucket::__set' => ['int', 'name'=>'string', 'value'=>'int'], -'Couchbase\Bucket::append' => ['Couchbase\Document|array', 'ids'=>'array|string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\Bucket::counter' => ['Couchbase\Document|array', 'ids'=>'array|string', 'delta='=>'int', 'options='=>'array'], -'Couchbase\Bucket::diag' => ['array', 'reportId='=>'string'], -'Couchbase\Bucket::get' => ['Couchbase\Document|array', 'ids'=>'array|string', 'options='=>'array'], -'Couchbase\Bucket::getAndLock' => ['Couchbase\Document|array', 'ids'=>'array|string', 'lockTime'=>'int', 'options='=>'array'], -'Couchbase\Bucket::getAndTouch' => ['Couchbase\Document|array', 'ids'=>'array|string', 'expiry'=>'int', 'options='=>'array'], -'Couchbase\Bucket::getFromReplica' => ['Couchbase\Document|array', 'ids'=>'array|string', 'options='=>'array'], -'Couchbase\Bucket::insert' => ['Couchbase\Document|array', 'ids'=>'array|string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\Bucket::listExists' => ['bool', 'id'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::listGet' => ['mixed', 'id'=>'string', 'index'=>'int'], -'Couchbase\Bucket::listPush' => ['', 'id'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::listRemove' => ['', 'id'=>'string', 'index'=>'int'], -'Couchbase\Bucket::listSet' => ['', 'id'=>'string', 'index'=>'int', 'value'=>'mixed'], -'Couchbase\Bucket::listShift' => ['', 'id'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::listSize' => ['int', 'id'=>'string'], -'Couchbase\Bucket::lookupIn' => ['Couchbase\LookupInBuilder', 'id'=>'string'], -'Couchbase\Bucket::manager' => ['Couchbase\BucketManager'], -'Couchbase\Bucket::mapAdd' => ['', 'id'=>'string', 'key'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::mapGet' => ['mixed', 'id'=>'string', 'key'=>'string'], -'Couchbase\Bucket::mapRemove' => ['', 'id'=>'string', 'key'=>'string'], -'Couchbase\Bucket::mapSize' => ['int', 'id'=>'string'], -'Couchbase\Bucket::mutateIn' => ['Couchbase\MutateInBuilder', 'id'=>'string', 'cas'=>'string'], -'Couchbase\Bucket::ping' => ['array', 'services='=>'int', 'reportId='=>'string'], -'Couchbase\Bucket::prepend' => ['Couchbase\Document|array', 'ids'=>'array|string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\Bucket::query' => ['object', 'query'=>'Couchbase\AnalyticsQuery|Couchbase\N1qlQuery|Couchbase\SearchQuery|Couchbase\SpatialViewQuery|Couchbase\ViewQuery', 'jsonAsArray='=>'bool|false'], -'Couchbase\Bucket::queueAdd' => ['', 'id'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::queueExists' => ['bool', 'id'=>'string', 'value'=>'mixed'], -'Couchbase\Bucket::queueRemove' => ['mixed', 'id'=>'string'], -'Couchbase\Bucket::queueSize' => ['int', 'id'=>'string'], -'Couchbase\Bucket::remove' => ['Couchbase\Document|array', 'ids'=>'array|string', 'options='=>'array'], -'Couchbase\Bucket::replace' => ['Couchbase\Document|array', 'ids'=>'array|string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\Bucket::retrieveIn' => ['Couchbase\DocumentFragment', 'id'=>'string', '...paths='=>'array'], -'Couchbase\Bucket::setAdd' => ['', 'id'=>'string', 'value'=>'bool|float|int|string'], -'Couchbase\Bucket::setExists' => ['bool', 'id'=>'string', 'value'=>'bool|float|int|string'], -'Couchbase\Bucket::setRemove' => ['', 'id'=>'string', 'value'=>'bool|float|int|string'], -'Couchbase\Bucket::setSize' => ['int', 'id'=>'string'], -'Couchbase\Bucket::setTranscoder' => ['', 'encoder'=>'callable', 'decoder'=>'callable'], -'Couchbase\Bucket::touch' => ['Couchbase\Document|array', 'ids'=>'array|string', 'expiry'=>'int', 'options='=>'array'], -'Couchbase\Bucket::unlock' => ['Couchbase\Document|array', 'ids'=>'array|string', 'options='=>'array'], -'Couchbase\Bucket::upsert' => ['Couchbase\Document|array', 'ids'=>'array|string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\BucketManager::__construct' => ['void'], -'Couchbase\BucketManager::createN1qlIndex' => ['', 'name'=>'string', 'fields'=>'array', 'whereClause='=>'string', 'ignoreIfExist='=>'bool|false', 'defer='=>'bool|false'], -'Couchbase\BucketManager::createN1qlPrimaryIndex' => ['', 'customName='=>'string', 'ignoreIfExist='=>'bool|false', 'defer='=>'bool|false'], -'Couchbase\BucketManager::dropN1qlIndex' => ['', 'name'=>'string', 'ignoreIfNotExist='=>'bool|false'], -'Couchbase\BucketManager::dropN1qlPrimaryIndex' => ['', 'customName='=>'string', 'ignoreIfNotExist='=>'bool|false'], -'Couchbase\BucketManager::flush' => [''], -'Couchbase\BucketManager::getDesignDocument' => ['array', 'name'=>'string'], -'Couchbase\BucketManager::info' => ['array'], -'Couchbase\BucketManager::insertDesignDocument' => ['', 'name'=>'string', 'document'=>'array'], -'Couchbase\BucketManager::listDesignDocuments' => ['array'], -'Couchbase\BucketManager::listN1qlIndexes' => ['array'], -'Couchbase\BucketManager::removeDesignDocument' => ['', 'name'=>'string'], -'Couchbase\BucketManager::upsertDesignDocument' => ['', 'name'=>'string', 'document'=>'array'], -'Couchbase\ClassicAuthenticator::bucket' => ['', 'name'=>'string', 'password'=>'string'], -'Couchbase\ClassicAuthenticator::cluster' => ['', 'username'=>'string', 'password'=>'string'], -'Couchbase\Cluster::__construct' => ['void', 'connstr'=>'string'], -'Couchbase\Cluster::authenticate' => ['null', 'authenticator'=>'Couchbase\Authenticator'], -'Couchbase\Cluster::authenticateAs' => ['null', 'username'=>'string', 'password'=>'string'], -'Couchbase\Cluster::manager' => ['Couchbase\ClusterManager', 'username='=>'string', 'password='=>'string'], -'Couchbase\Cluster::openBucket' => ['Couchbase\Bucket', 'name='=>'string', 'password='=>'string'], -'Couchbase\ClusterManager::__construct' => ['void'], -'Couchbase\ClusterManager::createBucket' => ['', 'name'=>'string', 'options='=>'array'], -'Couchbase\ClusterManager::getUser' => ['array', 'username'=>'string', 'domain='=>'int'], -'Couchbase\ClusterManager::info' => ['array'], -'Couchbase\ClusterManager::listBuckets' => ['array'], -'Couchbase\ClusterManager::listUsers' => ['array', 'domain='=>'int'], -'Couchbase\ClusterManager::removeBucket' => ['', 'name'=>'string'], -'Couchbase\ClusterManager::removeUser' => ['', 'name'=>'string', 'domain='=>'int'], -'Couchbase\ClusterManager::upsertUser' => ['', 'name'=>'string', 'settings'=>'Couchbase\UserSettings', 'domain='=>'int'], -'Couchbase\ConjunctionSearchQuery::__construct' => ['void'], -'Couchbase\ConjunctionSearchQuery::boost' => ['Couchbase\ConjunctionSearchQuery', 'boost'=>'float'], -'Couchbase\ConjunctionSearchQuery::every' => ['Couchbase\ConjunctionSearchQuery', '...queries='=>'array'], -'Couchbase\ConjunctionSearchQuery::jsonSerialize' => ['array'], -'Couchbase\DateRangeSearchFacet::__construct' => ['void'], -'Couchbase\DateRangeSearchFacet::addRange' => ['Couchbase\DateSearchFacet', 'name'=>'string', 'start'=>'int|string', 'end'=>'int|string'], -'Couchbase\DateRangeSearchFacet::jsonSerialize' => ['array'], -'Couchbase\DateRangeSearchQuery::__construct' => ['void'], -'Couchbase\DateRangeSearchQuery::boost' => ['Couchbase\DateRangeSearchQuery', 'boost'=>'float'], -'Couchbase\DateRangeSearchQuery::dateTimeParser' => ['Couchbase\DateRangeSearchQuery', 'dateTimeParser'=>'string'], -'Couchbase\DateRangeSearchQuery::end' => ['Couchbase\DateRangeSearchQuery', 'end'=>'int|string', 'inclusive='=>'bool|false'], -'Couchbase\DateRangeSearchQuery::field' => ['Couchbase\DateRangeSearchQuery', 'field'=>'string'], -'Couchbase\DateRangeSearchQuery::jsonSerialize' => ['array'], -'Couchbase\DateRangeSearchQuery::start' => ['Couchbase\DateRangeSearchQuery', 'start'=>'int|string', 'inclusive='=>'bool|true'], -'Couchbase\defaultDecoder' => ['mixed', 'bytes'=>'string', 'flags'=>'int', 'datatype'=>'int'], -'Couchbase\defaultEncoder' => ['array', 'value'=>'mixed'], -'Couchbase\DisjunctionSearchQuery::__construct' => ['void'], -'Couchbase\DisjunctionSearchQuery::boost' => ['Couchbase\DisjunctionSearchQuery', 'boost'=>'float'], -'Couchbase\DisjunctionSearchQuery::either' => ['Couchbase\DisjunctionSearchQuery', '...queries='=>'array'], -'Couchbase\DisjunctionSearchQuery::jsonSerialize' => ['array'], -'Couchbase\DisjunctionSearchQuery::min' => ['Couchbase\DisjunctionSearchQuery', 'min'=>'int'], -'Couchbase\DocIdSearchQuery::__construct' => ['void'], -'Couchbase\DocIdSearchQuery::boost' => ['Couchbase\DocIdSearchQuery', 'boost'=>'float'], -'Couchbase\DocIdSearchQuery::docIds' => ['Couchbase\DocIdSearchQuery', '...documentIds='=>'array'], -'Couchbase\DocIdSearchQuery::field' => ['Couchbase\DocIdSearchQuery', 'field'=>'string'], -'Couchbase\DocIdSearchQuery::jsonSerialize' => ['array'], -'Couchbase\fastlzCompress' => ['string', 'data'=>'string'], -'Couchbase\fastlzDecompress' => ['string', 'data'=>'string'], -'Couchbase\GeoBoundingBoxSearchQuery::__construct' => ['void'], -'Couchbase\GeoBoundingBoxSearchQuery::boost' => ['Couchbase\GeoBoundingBoxSearchQuery', 'boost'=>'float'], -'Couchbase\GeoBoundingBoxSearchQuery::field' => ['Couchbase\GeoBoundingBoxSearchQuery', 'field'=>'string'], -'Couchbase\GeoBoundingBoxSearchQuery::jsonSerialize' => ['array'], -'Couchbase\GeoDistanceSearchQuery::__construct' => ['void'], -'Couchbase\GeoDistanceSearchQuery::boost' => ['Couchbase\GeoDistanceSearchQuery', 'boost'=>'float'], -'Couchbase\GeoDistanceSearchQuery::field' => ['Couchbase\GeoDistanceSearchQuery', 'field'=>'string'], -'Couchbase\GeoDistanceSearchQuery::jsonSerialize' => ['array'], -'Couchbase\LookupInBuilder::__construct' => ['void'], -'Couchbase\LookupInBuilder::execute' => ['Couchbase\DocumentFragment'], -'Couchbase\LookupInBuilder::exists' => ['Couchbase\LookupInBuilder', 'path'=>'string', 'options='=>'array'], -'Couchbase\LookupInBuilder::get' => ['Couchbase\LookupInBuilder', 'path'=>'string', 'options='=>'array'], -'Couchbase\LookupInBuilder::getCount' => ['Couchbase\LookupInBuilder', 'path'=>'string', 'options='=>'array'], -'Couchbase\MatchAllSearchQuery::__construct' => ['void'], -'Couchbase\MatchAllSearchQuery::boost' => ['Couchbase\MatchAllSearchQuery', 'boost'=>'float'], -'Couchbase\MatchAllSearchQuery::jsonSerialize' => ['array'], -'Couchbase\MatchNoneSearchQuery::__construct' => ['void'], -'Couchbase\MatchNoneSearchQuery::boost' => ['Couchbase\MatchNoneSearchQuery', 'boost'=>'float'], -'Couchbase\MatchNoneSearchQuery::jsonSerialize' => ['array'], -'Couchbase\MatchPhraseSearchQuery::__construct' => ['void'], -'Couchbase\MatchPhraseSearchQuery::analyzer' => ['Couchbase\MatchPhraseSearchQuery', 'analyzer'=>'string'], -'Couchbase\MatchPhraseSearchQuery::boost' => ['Couchbase\MatchPhraseSearchQuery', 'boost'=>'float'], -'Couchbase\MatchPhraseSearchQuery::field' => ['Couchbase\MatchPhraseSearchQuery', 'field'=>'string'], -'Couchbase\MatchPhraseSearchQuery::jsonSerialize' => ['array'], -'Couchbase\MatchSearchQuery::__construct' => ['void'], -'Couchbase\MatchSearchQuery::analyzer' => ['Couchbase\MatchSearchQuery', 'analyzer'=>'string'], -'Couchbase\MatchSearchQuery::boost' => ['Couchbase\MatchSearchQuery', 'boost'=>'float'], -'Couchbase\MatchSearchQuery::field' => ['Couchbase\MatchSearchQuery', 'field'=>'string'], -'Couchbase\MatchSearchQuery::fuzziness' => ['Couchbase\MatchSearchQuery', 'fuzziness'=>'int'], -'Couchbase\MatchSearchQuery::jsonSerialize' => ['array'], -'Couchbase\MatchSearchQuery::prefixLength' => ['Couchbase\MatchSearchQuery', 'prefixLength'=>'int'], -'Couchbase\MutateInBuilder::__construct' => ['void'], -'Couchbase\MutateInBuilder::arrayAddUnique' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::arrayAppend' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::arrayAppendAll' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'values'=>'array', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::arrayInsert' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\MutateInBuilder::arrayInsertAll' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'values'=>'array', 'options='=>'array'], -'Couchbase\MutateInBuilder::arrayPrepend' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::arrayPrependAll' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'values'=>'array', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::counter' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'delta'=>'int', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::execute' => ['Couchbase\DocumentFragment'], -'Couchbase\MutateInBuilder::insert' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::modeDocument' => ['', 'mode'=>'int'], -'Couchbase\MutateInBuilder::remove' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'options='=>'array'], -'Couchbase\MutateInBuilder::replace' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array'], -'Couchbase\MutateInBuilder::upsert' => ['Couchbase\MutateInBuilder', 'path'=>'string', 'value'=>'mixed', 'options='=>'array|bool'], -'Couchbase\MutateInBuilder::withExpiry' => ['Couchbase\MutateInBuilder', 'expiry'=>'Couchbase\expiry'], -'Couchbase\MutationState::__construct' => ['void'], -'Couchbase\MutationState::add' => ['', 'source'=>'Couchbase\Document|Couchbase\DocumentFragment|array'], -'Couchbase\MutationState::from' => ['Couchbase\MutationState', 'source'=>'Couchbase\Document|Couchbase\DocumentFragment|array'], -'Couchbase\MutationToken::__construct' => ['void'], -'Couchbase\MutationToken::bucketName' => ['string'], -'Couchbase\MutationToken::from' => ['', 'bucketName'=>'string', 'vbucketId'=>'int', 'vbucketUuid'=>'string', 'sequenceNumber'=>'string'], -'Couchbase\MutationToken::sequenceNumber' => ['string'], -'Couchbase\MutationToken::vbucketId' => ['int'], -'Couchbase\MutationToken::vbucketUuid' => ['string'], -'Couchbase\N1qlIndex::__construct' => ['void'], -'Couchbase\N1qlQuery::__construct' => ['void'], -'Couchbase\N1qlQuery::adhoc' => ['Couchbase\N1qlQuery', 'adhoc'=>'bool'], -'Couchbase\N1qlQuery::consistency' => ['Couchbase\N1qlQuery', 'consistency'=>'int'], -'Couchbase\N1qlQuery::consistentWith' => ['Couchbase\N1qlQuery', 'state'=>'Couchbase\MutationState'], -'Couchbase\N1qlQuery::crossBucket' => ['Couchbase\N1qlQuery', 'crossBucket'=>'bool'], -'Couchbase\N1qlQuery::fromString' => ['Couchbase\N1qlQuery', 'statement'=>'string'], -'Couchbase\N1qlQuery::maxParallelism' => ['Couchbase\N1qlQuery', 'maxParallelism'=>'int'], -'Couchbase\N1qlQuery::namedParams' => ['Couchbase\N1qlQuery', 'params'=>'array'], -'Couchbase\N1qlQuery::pipelineBatch' => ['Couchbase\N1qlQuery', 'pipelineBatch'=>'int'], -'Couchbase\N1qlQuery::pipelineCap' => ['Couchbase\N1qlQuery', 'pipelineCap'=>'int'], -'Couchbase\N1qlQuery::positionalParams' => ['Couchbase\N1qlQuery', 'params'=>'array'], -'Couchbase\N1qlQuery::readonly' => ['Couchbase\N1qlQuery', 'readonly'=>'bool'], -'Couchbase\N1qlQuery::scanCap' => ['Couchbase\N1qlQuery', 'scanCap'=>'int'], -'Couchbase\NumericRangeSearchFacet::__construct' => ['void'], -'Couchbase\NumericRangeSearchFacet::addRange' => ['Couchbase\NumericSearchFacet', 'name'=>'string', 'min'=>'float', 'max'=>'float'], -'Couchbase\NumericRangeSearchFacet::jsonSerialize' => ['array'], -'Couchbase\NumericRangeSearchQuery::__construct' => ['void'], -'Couchbase\NumericRangeSearchQuery::boost' => ['Couchbase\NumericRangeSearchQuery', 'boost'=>'float'], -'Couchbase\NumericRangeSearchQuery::field' => ['Couchbase\NumericRangeSearchQuery', 'field'=>'string'], -'Couchbase\NumericRangeSearchQuery::jsonSerialize' => ['array'], -'Couchbase\NumericRangeSearchQuery::max' => ['Couchbase\NumericRangeSearchQuery', 'max'=>'float', 'inclusive='=>'bool|false'], -'Couchbase\NumericRangeSearchQuery::min' => ['Couchbase\NumericRangeSearchQuery', 'min'=>'float', 'inclusive='=>'bool|true'], -'Couchbase\passthruDecoder' => ['string', 'bytes'=>'string', 'flags'=>'int', 'datatype'=>'int'], -'Couchbase\passthruEncoder' => ['array', 'value'=>'string'], -'Couchbase\PasswordAuthenticator::password' => ['Couchbase\PasswordAuthenticator', 'password'=>'string'], -'Couchbase\PasswordAuthenticator::username' => ['Couchbase\PasswordAuthenticator', 'username'=>'string'], -'Couchbase\PhraseSearchQuery::__construct' => ['void'], -'Couchbase\PhraseSearchQuery::boost' => ['Couchbase\PhraseSearchQuery', 'boost'=>'float'], -'Couchbase\PhraseSearchQuery::field' => ['Couchbase\PhraseSearchQuery', 'field'=>'string'], -'Couchbase\PhraseSearchQuery::jsonSerialize' => ['array'], -'Couchbase\PrefixSearchQuery::__construct' => ['void'], -'Couchbase\PrefixSearchQuery::boost' => ['Couchbase\PrefixSearchQuery', 'boost'=>'float'], -'Couchbase\PrefixSearchQuery::field' => ['Couchbase\PrefixSearchQuery', 'field'=>'string'], -'Couchbase\PrefixSearchQuery::jsonSerialize' => ['array'], -'Couchbase\QueryStringSearchQuery::__construct' => ['void'], -'Couchbase\QueryStringSearchQuery::boost' => ['Couchbase\QueryStringSearchQuery', 'boost'=>'float'], -'Couchbase\QueryStringSearchQuery::jsonSerialize' => ['array'], -'Couchbase\RegexpSearchQuery::__construct' => ['void'], -'Couchbase\RegexpSearchQuery::boost' => ['Couchbase\RegexpSearchQuery', 'boost'=>'float'], -'Couchbase\RegexpSearchQuery::field' => ['Couchbase\RegexpSearchQuery', 'field'=>'string'], -'Couchbase\RegexpSearchQuery::jsonSerialize' => ['array'], -'Couchbase\SearchQuery::__construct' => ['void', 'indexName'=>'string', 'queryPart'=>'Couchbase\SearchQueryPart'], -'Couchbase\SearchQuery::addFacet' => ['Couchbase\SearchQuery', 'name'=>'string', 'facet'=>'Couchbase\SearchFacet'], -'Couchbase\SearchQuery::boolean' => ['Couchbase\BooleanSearchQuery'], -'Couchbase\SearchQuery::booleanField' => ['Couchbase\BooleanFieldSearchQuery', 'value'=>'bool'], -'Couchbase\SearchQuery::conjuncts' => ['Couchbase\ConjunctionSearchQuery', '...queries='=>'array'], -'Couchbase\SearchQuery::consistentWith' => ['Couchbase\SearchQuery', 'state'=>'Couchbase\MutationState'], -'Couchbase\SearchQuery::dateRange' => ['Couchbase\DateRangeSearchQuery'], -'Couchbase\SearchQuery::dateRangeFacet' => ['Couchbase\DateRangeSearchFacet', 'field'=>'string', 'limit'=>'int'], -'Couchbase\SearchQuery::disjuncts' => ['Couchbase\DisjunctionSearchQuery', '...queries='=>'array'], -'Couchbase\SearchQuery::docId' => ['Couchbase\DocIdSearchQuery', '...documentIds='=>'array'], -'Couchbase\SearchQuery::explain' => ['Couchbase\SearchQuery', 'explain'=>'bool'], -'Couchbase\SearchQuery::fields' => ['Couchbase\SearchQuery', '...fields='=>'array'], -'Couchbase\SearchQuery::geoBoundingBox' => ['Couchbase\GeoBoundingBoxSearchQuery', 'topLeftLongitude'=>'float', 'topLeftLatitude'=>'float', 'bottomRightLongitude'=>'float', 'bottomRightLatitude'=>'float'], -'Couchbase\SearchQuery::geoDistance' => ['Couchbase\GeoDistanceSearchQuery', 'longitude'=>'float', 'latitude'=>'float', 'distance'=>'string'], -'Couchbase\SearchQuery::highlight' => ['Couchbase\SearchQuery', 'style'=>'string', '...fields='=>'array'], -'Couchbase\SearchQuery::jsonSerialize' => ['array'], -'Couchbase\SearchQuery::limit' => ['Couchbase\SearchQuery', 'limit'=>'int'], -'Couchbase\SearchQuery::match' => ['Couchbase\MatchSearchQuery', 'match'=>'string'], -'Couchbase\SearchQuery::matchAll' => ['Couchbase\MatchAllSearchQuery'], -'Couchbase\SearchQuery::matchNone' => ['Couchbase\MatchNoneSearchQuery'], -'Couchbase\SearchQuery::matchPhrase' => ['Couchbase\MatchPhraseSearchQuery', '...terms='=>'array'], -'Couchbase\SearchQuery::numericRange' => ['Couchbase\NumericRangeSearchQuery'], -'Couchbase\SearchQuery::numericRangeFacet' => ['Couchbase\NumericRangeSearchFacet', 'field'=>'string', 'limit'=>'int'], -'Couchbase\SearchQuery::prefix' => ['Couchbase\PrefixSearchQuery', 'prefix'=>'string'], -'Couchbase\SearchQuery::queryString' => ['Couchbase\QueryStringSearchQuery', 'queryString'=>'string'], -'Couchbase\SearchQuery::regexp' => ['Couchbase\RegexpSearchQuery', 'regexp'=>'string'], -'Couchbase\SearchQuery::serverSideTimeout' => ['Couchbase\SearchQuery', 'serverSideTimeout'=>'int'], -'Couchbase\SearchQuery::skip' => ['Couchbase\SearchQuery', 'skip'=>'int'], -'Couchbase\SearchQuery::sort' => ['Couchbase\SearchQuery', '...sort='=>'array'], -'Couchbase\SearchQuery::term' => ['Couchbase\TermSearchQuery', 'term'=>'string'], -'Couchbase\SearchQuery::termFacet' => ['Couchbase\TermSearchFacet', 'field'=>'string', 'limit'=>'int'], -'Couchbase\SearchQuery::termRange' => ['Couchbase\TermRangeSearchQuery'], -'Couchbase\SearchQuery::wildcard' => ['Couchbase\WildcardSearchQuery', 'wildcard'=>'string'], -'Couchbase\SpatialViewQuery::__construct' => ['void'], -'Couchbase\SpatialViewQuery::bbox' => ['Couchbase\SpatialViewQuery', 'bbox'=>'array'], -'Couchbase\SpatialViewQuery::consistency' => ['Couchbase\SpatialViewQuery', 'consistency'=>'int'], -'Couchbase\SpatialViewQuery::custom' => ['', 'customParameters'=>'array'], -'Couchbase\SpatialViewQuery::encode' => ['array'], -'Couchbase\SpatialViewQuery::endRange' => ['Couchbase\SpatialViewQuery', 'range'=>'array'], -'Couchbase\SpatialViewQuery::limit' => ['Couchbase\SpatialViewQuery', 'limit'=>'int'], -'Couchbase\SpatialViewQuery::order' => ['Couchbase\SpatialViewQuery', 'order'=>'int'], -'Couchbase\SpatialViewQuery::skip' => ['Couchbase\SpatialViewQuery', 'skip'=>'int'], -'Couchbase\SpatialViewQuery::startRange' => ['Couchbase\SpatialViewQuery', 'range'=>'array'], -'Couchbase\TermRangeSearchQuery::__construct' => ['void'], -'Couchbase\TermRangeSearchQuery::boost' => ['Couchbase\TermRangeSearchQuery', 'boost'=>'float'], -'Couchbase\TermRangeSearchQuery::field' => ['Couchbase\TermRangeSearchQuery', 'field'=>'string'], -'Couchbase\TermRangeSearchQuery::jsonSerialize' => ['array'], -'Couchbase\TermRangeSearchQuery::max' => ['Couchbase\TermRangeSearchQuery', 'max'=>'string', 'inclusive='=>'bool|false'], -'Couchbase\TermRangeSearchQuery::min' => ['Couchbase\TermRangeSearchQuery', 'min'=>'string', 'inclusive='=>'bool|true'], -'Couchbase\TermSearchFacet::__construct' => ['void'], -'Couchbase\TermSearchFacet::jsonSerialize' => ['array'], -'Couchbase\TermSearchQuery::__construct' => ['void'], -'Couchbase\TermSearchQuery::boost' => ['Couchbase\TermSearchQuery', 'boost'=>'float'], -'Couchbase\TermSearchQuery::field' => ['Couchbase\TermSearchQuery', 'field'=>'string'], -'Couchbase\TermSearchQuery::fuzziness' => ['Couchbase\TermSearchQuery', 'fuzziness'=>'int'], -'Couchbase\TermSearchQuery::jsonSerialize' => ['array'], -'Couchbase\TermSearchQuery::prefixLength' => ['Couchbase\TermSearchQuery', 'prefixLength'=>'int'], -'Couchbase\UserSettings::fullName' => ['Couchbase\UserSettings', 'fullName'=>'string'], -'Couchbase\UserSettings::password' => ['Couchbase\UserSettings', 'password'=>'string'], -'Couchbase\UserSettings::role' => ['Couchbase\UserSettings', 'role'=>'string', 'bucket='=>'string'], -'Couchbase\ViewQuery::__construct' => ['void'], -'Couchbase\ViewQuery::consistency' => ['Couchbase\ViewQuery', 'consistency'=>'int'], -'Couchbase\ViewQuery::custom' => ['Couchbase\ViewQuery', 'customParameters'=>'array'], -'Couchbase\ViewQuery::encode' => ['array'], -'Couchbase\ViewQuery::from' => ['Couchbase\ViewQuery', 'designDocumentName'=>'string', 'viewName'=>'string'], -'Couchbase\ViewQuery::fromSpatial' => ['Couchbase\SpatialViewQuery', 'designDocumentName'=>'string', 'viewName'=>'string'], -'Couchbase\ViewQuery::group' => ['Couchbase\ViewQuery', 'group'=>'bool'], -'Couchbase\ViewQuery::groupLevel' => ['Couchbase\ViewQuery', 'groupLevel'=>'int'], -'Couchbase\ViewQuery::idRange' => ['Couchbase\ViewQuery', 'startKeyDocumentId'=>'string', 'endKeyDocumentId'=>'string'], -'Couchbase\ViewQuery::key' => ['Couchbase\ViewQuery', 'key'=>'mixed'], -'Couchbase\ViewQuery::keys' => ['Couchbase\ViewQuery', 'keys'=>'array'], -'Couchbase\ViewQuery::limit' => ['Couchbase\ViewQuery', 'limit'=>'int'], -'Couchbase\ViewQuery::order' => ['Couchbase\ViewQuery', 'order'=>'int'], -'Couchbase\ViewQuery::range' => ['Couchbase\ViewQuery', 'startKey'=>'mixed', 'endKey'=>'mixed', 'inclusiveEnd='=>'bool|false'], -'Couchbase\ViewQuery::reduce' => ['Couchbase\ViewQuery', 'reduce'=>'bool'], -'Couchbase\ViewQuery::skip' => ['Couchbase\ViewQuery', 'skip'=>'int'], -'Couchbase\ViewQueryEncodable::encode' => ['array'], -'Couchbase\WildcardSearchQuery::__construct' => ['void'], -'Couchbase\WildcardSearchQuery::boost' => ['Couchbase\WildcardSearchQuery', 'boost'=>'float'], -'Couchbase\WildcardSearchQuery::field' => ['Couchbase\WildcardSearchQuery', 'field'=>'string'], -'Couchbase\WildcardSearchQuery::jsonSerialize' => ['array'], -'Couchbase\zlibCompress' => ['string', 'data'=>'string'], -'Couchbase\zlibDecompress' => ['string', 'data'=>'string'], -'count' => ['0|positive-int', 'var'=>'Countable|array', 'mode='=>'int'], -'count_chars' => ['mixed', 'input'=>'string', 'mode='=>'int'], +'count' => ['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'], @@ -1375,7 +1004,7 @@ 'crash' => [''], 'crc32' => ['int', 'str'=>'string'], 'create_function' => ['string', 'args'=>'string', 'code'=>'string'], -'crypt' => ['string', 'str'=>'string', 'salt='=>'string'], +'crypt' => ['non-empty-string', 'str'=>'string', 'salt='=>'string'], 'ctype_alnum' => ['bool', 'c'=>'mixed'], 'ctype_alpha' => ['bool', 'c'=>'mixed'], 'ctype_cntrl' => ['bool', 'c'=>'mixed'], @@ -1494,14 +1123,14 @@ 'curl_exec' => ['bool|string', 'ch'=>'resource'], 'curl_file_create' => ['CURLFile', 'filename'=>'string', 'mimetype='=>'string', 'postfilename='=>'string'], 'curl_getinfo' => ['mixed', 'ch'=>'resource', 'option='=>'int'], -'curl_init' => ['resource|false', 'url='=>'string'], +'curl_init' => ['__benevolent', 'url='=>'string'], 'curl_multi_add_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_close' => ['void', 'mh'=>'resource'], 'curl_multi_errno' => ['int', 'mh'=>'resource'], 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], -'curl_multi_getcontent' => ['string', 'ch'=>'resource'], +'curl_multi_getcontent' => ['string|null', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], -'curl_multi_init' => ['resource|false'], +'curl_multi_init' => ['resource'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_select' => ['int', 'mh'=>'resource', 'timeout='=>'float'], 'curl_multi_setopt' => ['bool', 'mh'=>'resource', 'option'=>'int', 'value'=>'mixed'], @@ -1534,25 +1163,25 @@ 'cyrus_unbind' => ['bool', 'connection'=>'resource', 'trigger_name'=>'string'], 'date' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'date_add' => ['DateTime|false', 'object'=>'', 'interval'=>''], -'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?\DateTimeZone'], -'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], +'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?DateTimeZone'], +'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'date_date_set' => ['DateTime|false', 'object'=>'', 'year'=>'', 'month'=>'', 'day'=>''], 'date_default_timezone_get' => ['string'], 'date_default_timezone_set' => ['bool', 'timezone_identifier'=>'string'], 'date_diff' => ['DateInterval', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'obj'=>'DateTimeInterface', 'format'=>'string'], -'date_get_last_errors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'date_get_last_errors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'date_interval_create_from_date_string' => ['DateInterval|false', 'time'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], '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='=>''], @@ -1564,7 +1193,7 @@ 'datefmt_format' => ['string|false', 'fmt'=>'IntlDateFormatter', 'value'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int|false', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar|false', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'fmt'=>'IntlDateFormatter'], @@ -1574,24 +1203,24 @@ 'datefmt_get_timezone' => ['IntlTimeZone|false'], 'datefmt_get_timezone_id' => ['string|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'fmt'=>'IntlDateFormatter'], -'datefmt_localtime' => ['array|bool|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], -'datefmt_parse' => ['int|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_localtime' => ['array|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_parse' => ['int|float|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], 'datefmt_set_calendar' => ['bool', 'fmt'=>'IntlDateFormatter', 'which'=>'int'], -'datefmt_set_lenient' => ['?bool', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], +'datefmt_set_lenient' => ['void', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'fmt'=>'IntlDateFormatter', 'pattern'=>'string'], 'datefmt_set_timezone' => ['bool', 'zone'=>'mixed'], 'datefmt_set_timezone_id' => ['bool', 'fmt'=>'IntlDateFormatter', 'zone'=>'string'], 'DateInterval::__construct' => ['void', 'spec'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], -'DateInterval::createFromDateString' => ['DateInterval', 'time'=>'string'], +'DateInterval::createFromDateString' => ['DateInterval|false', 'time'=>'string'], 'DateInterval::format' => ['string', 'format'=>'string'], 'DatePeriod::__construct' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'recur'=>'int', 'options='=>'int'], 'DatePeriod::__construct\'1' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'end'=>'DateTimeInterface', 'options='=>'int'], '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 +1230,11 @@ 'DateTime::createFromImmutable' => ['static', 'object'=>'DateTimeImmutable'], 'DateTime::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTime::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int'], 'DateTime::getTimezone' => ['DateTimeZone'], -'DateTime::modify' => ['static', 'modify'=>'string'], +'DateTime::modify' => ['__benevolent', 'modify'=>'string'], 'DateTime::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTime::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTime::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], @@ -1620,11 +1249,11 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string', 'format'=>'string'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int'], 'DateTimeImmutable::getTimezone' => ['DateTimeZone'], -'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], +'DateTimeImmutable::modify' => ['__benevolent', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], @@ -1639,15 +1268,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 +1288,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 +1304,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 +1319,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'], @@ -1787,7 +1416,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?:list,object?:object}>', 'options='=>'int|bool', 'limit='=>'int'], 'debug_print_backtrace' => ['void', 'options='=>'int|bool', 'limit='=>'int'], 'debug_zval_dump' => ['void', '...var'=>'mixed'], 'debugger_connect' => [''], @@ -1855,7 +1484,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 +1493,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 +1513,7 @@ 'DomainException::getLine' => ['int'], 'DomainException::getMessage' => ['string'], 'DomainException::getPrevious' => ['Throwable|DomainException|null'], -'DomainException::getTrace' => ['array'], +'DomainException::getTrace' => ['list\',args?:list,object?:object}>'], 'DomainException::getTraceAsString' => ['string'], 'DOMAttr::__construct' => ['void', 'name'=>'string', 'value='=>'string'], 'DOMAttr::isId' => ['bool'], @@ -1900,29 +1529,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'], @@ -1975,24 +1604,24 @@ 'DOMNamedNodeMap::getNamedItemNS' => ['?DOMNode', 'namespaceuri'=>'string', 'localname'=>'string'], 'DOMNamedNodeMap::item' => ['?DOMNode', 'index'=>'int'], 'DomNode::add_namespace' => ['bool', 'uri'=>'string', 'prefix'=>'string'], -'DomNode::append_child' => ['DOMNode', 'newnode'=>'DOMNode'], -'DOMNode::appendChild' => ['DOMNode', 'newnode'=>'DOMNode'], +'DomNode::append_child' => ['__benevolent', 'newnode'=>'DOMNode'], +'DOMNode::appendChild' => ['__benevolent', 'newnode'=>'DOMNode'], 'DOMNode::C14N' => ['string', 'exclusive='=>'bool', 'with_comments='=>'bool', 'xpath='=>'array', 'ns_prefixes='=>'array'], 'DOMNode::C14NFile' => ['int', 'uri='=>'string', 'exclusive='=>'bool', 'with_comments='=>'bool', 'xpath='=>'array', 'ns_prefixes='=>'array'], -'DOMNode::cloneNode' => ['DOMNode', 'deep='=>'bool'], +'DOMNode::cloneNode' => ['__benevolent', 'deep='=>'bool'], 'DOMNode::getLineNo' => ['int'], 'DOMNode::getNodePath' => ['?string'], 'DOMNode::hasAttributes' => ['bool'], 'DOMNode::hasChildNodes' => ['bool'], -'DOMNode::insertBefore' => ['DOMNode', 'newnode'=>'DOMNode', 'refnode='=>'DOMNode'], +'DOMNode::insertBefore' => ['__benevolent', 'newnode'=>'DOMNode', 'refnode='=>'DOMNode'], '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'], +'DOMNode::removeChild' => ['__benevolent', 'oldnode'=>'DOMNode'], +'DOMNode::replaceChild' => ['__benevolent', 'newnode'=>'DOMNode', 'oldnode'=>'DOMNode'], 'DOMNodeList::count' => ['0|positive-int'], 'DOMNodeList::item' => ['?DOMNode', 'index'=>'int'], 'DOMProcessingInstruction::__construct' => ['void', 'name'=>'string', 'value'=>'string'], @@ -2001,7 +1630,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'], @@ -2021,279 +1650,108 @@ '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'], -'Ds\Collection::clear' => ['void'], -'Ds\Collection::copy' => ['Ds\Collection'], -'Ds\Collection::isEmpty' => ['bool'], -'Ds\Collection::toArray' => ['array'], +'doubleval' => ['float', 'var'=>'scalar|array|resource|null'], 'Ds\Deque::__construct' => ['void', 'values='=>'mixed'], -'Ds\Deque::allocate' => ['void', 'capacity'=>'int'], -'Ds\Deque::apply' => ['void', 'callback'=>'callable'], -'Ds\Deque::capacity' => ['int'], -'Ds\Deque::clear' => ['void'], -'Ds\Deque::contains' => ['bool', '...values='=>'mixed'], -'Ds\Deque::copy' => ['Ds\Deque'], 'Ds\Deque::count' => ['0|positive-int'], -'Ds\Deque::filter' => ['Ds\Deque', 'callback='=>'callable'], -'Ds\Deque::find' => ['mixed', 'value'=>'mixed'], -'Ds\Deque::first' => ['mixed'], -'Ds\Deque::get' => ['void', 'index'=>'int'], -'Ds\Deque::insert' => ['void', 'index'=>'int', '...values='=>'mixed'], -'Ds\Deque::isEmpty' => ['bool'], -'Ds\Deque::join' => ['string', 'glue='=>'string'], 'Ds\Deque::jsonSerialize' => ['array'], -'Ds\Deque::last' => ['mixed'], -'Ds\Deque::map' => ['Ds\Deque', 'callback'=>'callable'], -'Ds\Deque::merge' => ['Ds\Deque', 'values'=>'mixed'], -'Ds\Deque::pop' => ['mixed'], -'Ds\Deque::push' => ['void', '...values='=>'mixed'], -'Ds\Deque::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], -'Ds\Deque::remove' => ['mixed', 'index'=>'int'], -'Ds\Deque::reverse' => ['void'], -'Ds\Deque::reversed' => ['Ds\Deque'], -'Ds\Deque::rotate' => ['void', 'rotations'=>'int'], -'Ds\Deque::set' => ['void', 'index'=>'int', 'value'=>'mixed'], -'Ds\Deque::shift' => ['mixed'], -'Ds\Deque::slice' => ['Ds\Deque', 'index'=>'int', 'length='=>'?int'], -'Ds\Deque::sort' => ['void', 'comparator='=>'callable'], -'Ds\Deque::sorted' => ['Ds\Deque', 'comparator='=>'callable'], -'Ds\Deque::sum' => ['int|float'], -'Ds\Deque::toArray' => ['array'], -'Ds\Deque::unshift' => ['void', '...values='=>'mixed'], -'Ds\Hashable::equals' => ['bool', 'obj'=>'mixed'], -'Ds\Hashable::hash' => ['mixed'], 'Ds\Map::__construct' => ['void', 'values='=>'mixed'], 'Ds\Map::allocate' => ['void', 'capacity'=>'int'], 'Ds\Map::apply' => ['void', 'callback'=>'callable'], -'Ds\Map::capacity' => ['int'], -'Ds\Map::clear' => ['void'], -'Ds\Map::copy' => ['Ds\Map'], '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'], -'Ds\Map::get' => ['mixed', 'key'=>'mixed', 'default='=>'mixed'], -'Ds\Map::hasKey' => ['bool', 'key'=>'mixed'], -'Ds\Map::hasValue' => ['bool', 'value'=>'mixed'], -'Ds\Map::intersect' => ['Ds\Map', 'map'=>'Ds\Map'], -'Ds\Map::isEmpty' => ['bool'], 'Ds\Map::jsonSerialize' => ['array'], -'Ds\Map::keys' => ['Ds\Set'], 'Ds\Map::ksort' => ['void', 'comparator='=>'callable'], -'Ds\Map::ksorted' => ['Ds\Map', 'comparator='=>'callable'], -'Ds\Map::last' => ['Ds\Pair'], -'Ds\Map::map' => ['Ds\Map', 'callback'=>'callable'], -'Ds\Map::merge' => ['Ds\Map', 'values'=>'mixed'], -'Ds\Map::pairs' => ['Ds\Sequence'], 'Ds\Map::put' => ['void', 'key'=>'mixed', 'value'=>'mixed'], 'Ds\Map::putAll' => ['void', 'values'=>'mixed'], -'Ds\Map::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], -'Ds\Map::remove' => ['mixed', 'key'=>'mixed', 'default='=>'mixed'], 'Ds\Map::reverse' => ['void'], -'Ds\Map::reversed' => ['Ds\Map'], -'Ds\Map::skip' => ['Ds\Pair', 'position'=>'int'], -'Ds\Map::slice' => ['Ds\Map', 'index'=>'int', 'length='=>'?int'], 'Ds\Map::sort' => ['void', 'comparator='=>'callable'], -'Ds\Map::sorted' => ['Ds\Map', 'comparator='=>'callable'], -'Ds\Map::sum' => ['int|float'], -'Ds\Map::toArray' => ['array'], -'Ds\Map::union' => ['Ds\Map', 'map'=>'Ds\Map'], -'Ds\Map::values' => ['Ds\Sequence'], -'Ds\Map::xor' => ['Ds\Map', 'map'=>'Ds\Map'], -'Ds\Pair::__construct' => ['void', 'key='=>'mixed', 'value='=>'mixed'], -'Ds\Pair::copy' => ['Ds\Pair'], 'Ds\Pair::jsonSerialize' => ['array'], -'Ds\Pair::toArray' => ['array'], -'Ds\PriorityQueue::__construct' => ['void'], -'Ds\PriorityQueue::allocate' => ['void', 'capacity'=>'int'], -'Ds\PriorityQueue::capacity' => ['int'], -'Ds\PriorityQueue::clear' => ['void'], -'Ds\PriorityQueue::copy' => ['Ds\PriorityQueue'], 'Ds\PriorityQueue::count' => ['0|positive-int'], -'Ds\PriorityQueue::isEmpty' => ['bool'], 'Ds\PriorityQueue::jsonSerialize' => ['array'], -'Ds\PriorityQueue::peek' => ['mixed'], -'Ds\PriorityQueue::pop' => ['mixed'], 'Ds\PriorityQueue::push' => ['void', 'value'=>'mixed', 'priority'=>'int'], -'Ds\PriorityQueue::toArray' => ['array'], -'Ds\Queue::__construct' => ['void', 'values='=>'mixed'], 'Ds\Queue::allocate' => ['void', 'capacity'=>'int'], -'Ds\Queue::capacity' => ['int'], -'Ds\Queue::clear' => ['void'], -'Ds\Queue::copy' => ['Ds\Queue'], 'Ds\Queue::count' => ['0|positive-int'], -'Ds\Queue::isEmpty' => ['bool'], 'Ds\Queue::jsonSerialize' => ['array'], -'Ds\Queue::peek' => ['mixed'], -'Ds\Queue::pop' => ['mixed'], 'Ds\Queue::push' => ['void', '...values='=>'mixed'], -'Ds\Queue::toArray' => ['array'], -'Ds\Sequence::allocate' => ['void', 'capacity'=>'int'], -'Ds\Sequence::apply' => ['void', 'callback'=>'callable'], -'Ds\Sequence::capacity' => ['int'], -'Ds\Sequence::contains' => ['bool', '...values='=>'mixed'], -'Ds\Sequence::filter' => ['Ds\Sequence', 'callback='=>'callable'], -'Ds\Sequence::find' => ['mixed', 'value'=>'mixed'], -'Ds\Sequence::first' => ['mixed'], -'Ds\Sequence::get' => ['mixed', 'index'=>'int'], -'Ds\Sequence::insert' => ['void', 'index'=>'int', '...values='=>'mixed'], -'Ds\Sequence::join' => ['string', 'glue='=>'string'], -'Ds\Sequence::last' => ['void'], -'Ds\Sequence::map' => ['Ds\Sequence', 'callback'=>'callable'], -'Ds\Sequence::merge' => ['Ds\Sequence', 'values'=>'mixed'], -'Ds\Sequence::pop' => ['mixed'], -'Ds\Sequence::push' => ['void', '...values='=>'mixed'], -'Ds\Sequence::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], -'Ds\Sequence::remove' => ['mixed', 'index'=>'int'], -'Ds\Sequence::reverse' => ['void'], -'Ds\Sequence::reversed' => ['Ds\Sequence'], -'Ds\Sequence::rotate' => ['void', 'rotations'=>'int'], -'Ds\Sequence::set' => ['void', 'index'=>'int', 'value'=>'mixed'], -'Ds\Sequence::shift' => ['mixed'], -'Ds\Sequence::slice' => ['Ds\Sequence', 'index'=>'int', 'length='=>'?int'], -'Ds\Sequence::sort' => ['void', 'comparator='=>'callable'], -'Ds\Sequence::sorted' => ['Ds\Sequence', 'comparator='=>'callable'], -'Ds\Sequence::sum' => ['int|float'], -'Ds\Sequence::unshift' => ['void', '...values='=>'mixed'], -'Ds\Set::__construct' => ['void', 'values='=>'mixed'], 'Ds\Set::add' => ['void', '...values='=>'mixed'], 'Ds\Set::allocate' => ['void', 'capacity'=>'int'], -'Ds\Set::capacity' => ['int'], 'Ds\Set::clear' => ['void'], -'Ds\Set::contains' => ['bool', '...values='=>'mixed'], -'Ds\Set::copy' => ['Ds\Set'], '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'], -'Ds\Set::get' => ['mixed', 'index'=>'int'], -'Ds\Set::intersect' => ['Ds\Set', 'set'=>'Ds\Set'], -'Ds\Set::isEmpty' => ['bool'], 'Ds\Set::join' => ['string', 'glue='=>'string'], 'Ds\Set::jsonSerialize' => ['array'], -'Ds\Set::last' => ['mixed'], -'Ds\Set::merge' => ['Ds\Set', 'values'=>'mixed'], -'Ds\Set::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], 'Ds\Set::remove' => ['void', '...values='=>'mixed'], 'Ds\Set::reverse' => ['void'], -'Ds\Set::reversed' => ['Ds\Set'], -'Ds\Set::slice' => ['Ds\Set', 'index'=>'int', 'length='=>'?int'], 'Ds\Set::sort' => ['void', 'comparator='=>'callable'], -'Ds\Set::sorted' => ['Ds\Set', 'comparator='=>'callable'], -'Ds\Set::sum' => ['int|float'], -'Ds\Set::toArray' => ['array'], -'Ds\Set::union' => ['Ds\Set', 'set'=>'Ds\Set'], -'Ds\Set::xor' => ['Ds\Set', 'set'=>'Ds\Set'], -'Ds\Stack::__construct' => ['void', 'values='=>'mixed'], 'Ds\Stack::allocate' => ['void', 'capacity'=>'int'], -'Ds\Stack::capacity' => ['int'], -'Ds\Stack::clear' => ['void'], -'Ds\Stack::copy' => ['Ds\Stack'], 'Ds\Stack::count' => ['0|positive-int'], -'Ds\Stack::isEmpty' => ['bool'], 'Ds\Stack::jsonSerialize' => ['array'], -'Ds\Stack::peek' => ['mixed'], -'Ds\Stack::pop' => ['mixed'], -'Ds\Stack::push' => ['void', '...values='=>'mixed'], -'Ds\Stack::toArray' => ['array'], 'Ds\Vector::__construct' => ['void', 'values='=>'mixed'], -'Ds\Vector::allocate' => ['void', 'capacity'=>'int'], -'Ds\Vector::apply' => ['void', 'callback'=>'callable'], -'Ds\Vector::capacity' => ['int'], -'Ds\Vector::clear' => ['void'], -'Ds\Vector::contains' => ['bool', '...values='=>'mixed'], -'Ds\Vector::copy' => ['Ds\Vector'], 'Ds\Vector::count' => ['0|positive-int'], -'Ds\Vector::filter' => ['Ds\Vector', 'callback='=>'callable'], -'Ds\Vector::find' => ['mixed', 'value'=>'mixed'], -'Ds\Vector::first' => ['mixed'], -'Ds\Vector::get' => ['mixed', 'index'=>'int'], -'Ds\Vector::insert' => ['void', 'index'=>'int', '...values='=>'mixed'], -'Ds\Vector::isEmpty' => ['bool'], 'Ds\Vector::join' => ['string', 'glue='=>'string'], 'Ds\Vector::jsonSerialize' => ['array'], -'Ds\Vector::last' => ['mixed'], -'Ds\Vector::map' => ['Ds\Vector', 'callback'=>'callable'], -'Ds\Vector::merge' => ['Ds\Vector', 'values'=>'mixed'], -'Ds\Vector::pop' => ['mixed'], -'Ds\Vector::push' => ['void', '...values='=>'mixed'], -'Ds\Vector::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], -'Ds\Vector::remove' => ['mixed', 'index'=>'int'], -'Ds\Vector::reverse' => ['void'], -'Ds\Vector::reversed' => ['Ds\Vector'], -'Ds\Vector::rotate' => ['void', 'rotations'=>'int'], -'Ds\Vector::set' => ['void', 'index'=>'int', 'value'=>'mixed'], -'Ds\Vector::shift' => ['mixed'], -'Ds\Vector::slice' => ['Ds\Vector', 'index'=>'int', 'length='=>'?int'], -'Ds\Vector::sort' => ['void', 'comparator='=>'callable'], -'Ds\Vector::sorted' => ['Ds\Vector', 'comparator='=>'callable'], 'Ds\Vector::sum' => ['int|float'], -'Ds\Vector::toArray' => ['array'], 'Ds\Vector::unshift' => ['void', '...values='=>'mixed'], '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_busy' => ['resource|false', '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'], -'eio_chown' => ['resource', 'path'=>'string', 'uid'=>'int', 'gid='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_close' => ['resource', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_custom' => ['resource', 'execute'=>'callable', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_dup2' => ['resource', 'fd'=>'mixed', 'fd2'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_chmod' => ['resource|false', 'path'=>'string', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_chown' => ['resource|false', 'path'=>'string', 'uid'=>'int', 'gid='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_close' => ['resource|false', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_custom' => ['resource|false', 'execute'=>'callable', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_dup2' => ['resource|false', 'fd'=>'mixed', 'fd2'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_event_loop' => ['bool'], -'eio_fallocate' => ['resource', 'fd'=>'mixed', 'mode'=>'int', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_fchmod' => ['resource', 'fd'=>'mixed', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_fchown' => ['resource', 'fd'=>'mixed', 'uid'=>'int', 'gid='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_fdatasync' => ['resource', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_fstat' => ['resource', 'fd'=>'mixed', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_fstatvfs' => ['resource', 'fd'=>'mixed', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_fsync' => ['resource', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_ftruncate' => ['resource', 'fd'=>'mixed', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_futime' => ['resource', 'fd'=>'mixed', 'atime'=>'float', 'mtime'=>'float', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_fallocate' => ['resource|false', 'fd'=>'mixed', 'mode'=>'int', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_fchmod' => ['resource|false', 'fd'=>'mixed', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_fchown' => ['resource|false', 'fd'=>'mixed', 'uid'=>'int', 'gid='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_fdatasync' => ['resource|false', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_fstat' => ['resource|false', 'fd'=>'mixed', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_fstatvfs' => ['resource|false', 'fd'=>'mixed', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_fsync' => ['resource|false', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_ftruncate' => ['resource|false', 'fd'=>'mixed', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_futime' => ['resource|false', 'fd'=>'mixed', 'atime'=>'float', 'mtime'=>'float', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_get_event_stream' => ['mixed'], 'eio_get_last_error' => ['string', 'req'=>'resource'], -'eio_grp' => ['resource', 'callback'=>'callable', 'data='=>'string'], +'eio_grp' => ['resource|false', 'callback'=>'callable', 'data='=>'string'], 'eio_grp_add' => ['void', 'grp'=>'resource', 'req'=>'resource'], 'eio_grp_cancel' => ['void', 'grp'=>'resource'], 'eio_grp_limit' => ['void', 'grp'=>'resource', 'limit'=>'int'], 'eio_init' => ['void'], -'eio_link' => ['resource', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_lstat' => ['resource', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_mkdir' => ['resource', 'path'=>'string', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_mknod' => ['resource', 'path'=>'string', 'mode'=>'int', 'dev'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_nop' => ['resource', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_link' => ['resource|false', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_lstat' => ['resource|false', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_mkdir' => ['resource|false', 'path'=>'string', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_mknod' => ['resource|false', 'path'=>'string', 'mode'=>'int', 'dev'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_nop' => ['resource|false', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_npending' => ['int'], 'eio_nready' => ['int'], 'eio_nreqs' => ['int'], 'eio_nthreads' => ['int'], -'eio_open' => ['resource', 'path'=>'string', 'flags'=>'int', 'mode'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_open' => ['resource|false', 'path'=>'string', 'flags'=>'int', 'mode'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], 'eio_poll' => ['int'], -'eio_read' => ['resource', 'fd'=>'mixed', 'length'=>'int', 'offset'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_readahead' => ['resource', 'fd'=>'mixed', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_readdir' => ['resource', 'path'=>'string', 'flags'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], -'eio_readlink' => ['resource', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], -'eio_realpath' => ['resource', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], -'eio_rename' => ['resource', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_rmdir' => ['resource', 'path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_seek' => ['resource', 'fd'=>'mixed', 'offset'=>'int', 'whence'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_sendfile' => ['resource', 'out_fd'=>'mixed', 'in_fd'=>'mixed', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'string'], +'eio_read' => ['resource|false', 'fd'=>'mixed', 'length'=>'int', 'offset'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_readahead' => ['resource|false', 'fd'=>'mixed', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_readdir' => ['resource|false', 'path'=>'string', 'flags'=>'int', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], +'eio_readlink' => ['resource|false', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], +'eio_realpath' => ['resource|false', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'string'], +'eio_rename' => ['resource|false', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_rmdir' => ['resource|false', 'path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_seek' => ['resource|false', 'fd'=>'mixed', 'offset'=>'int', 'whence'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_sendfile' => ['resource|false', 'out_fd'=>'mixed', 'in_fd'=>'mixed', 'offset'=>'int', 'length'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'string'], 'eio_set_max_idle' => ['void', 'nthreads'=>'int'], 'eio_set_max_parallel' => ['void', 'nthreads'=>'int'], 'eio_set_max_poll_reqs' => ['void', 'nreqs'=>'int'], 'eio_set_max_poll_time' => ['void', 'nseconds'=>'float'], 'eio_set_min_parallel' => ['void', 'nthreads'=>'string'], -'eio_stat' => ['resource', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_statvfs' => ['resource', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], -'eio_symlink' => ['resource', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_sync' => ['resource', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_sync_file_range' => ['resource', 'fd'=>'mixed', 'offset'=>'int', 'nbytes'=>'int', 'flags'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_syncfs' => ['resource', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'eio_truncate' => ['resource', 'path'=>'string', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'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'], +'eio_stat' => ['resource|false', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_statvfs' => ['resource|false', 'path'=>'string', 'pri'=>'int', 'callback'=>'callable', 'data='=>'mixed'], +'eio_symlink' => ['resource|false', 'path'=>'string', 'new_path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_sync' => ['resource|false', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_sync_file_range' => ['resource|false', 'fd'=>'mixed', 'offset'=>'int', 'nbytes'=>'int', 'flags'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_syncfs' => ['resource|false', 'fd'=>'mixed', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_truncate' => ['resource|false', 'path'=>'string', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_unlink' => ['resource|false', 'path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_utime' => ['resource|false', 'path'=>'string', 'atime'=>'float', 'mtime'=>'float', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], +'eio_write' => ['resource|false', 'fd'=>'mixed', 'str'=>'string', 'length='=>'int', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'EmptyIterator::current' => ['mixed'], 'EmptyIterator::key' => ['mixed'], 'EmptyIterator::next' => ['void'], @@ -2333,11 +1791,11 @@ 'Error::getLine' => ['int'], 'Error::getMessage' => ['string'], 'Error::getPrevious' => ['Throwable|Error|null'], -'Error::getTrace' => ['array'], +'Error::getTrace' => ['list\',args?:list,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 +1806,7 @@ 'ErrorException::getMessage' => ['string'], 'ErrorException::getPrevious' => ['Throwable|ErrorException|null'], 'ErrorException::getSeverity' => ['int'], -'ErrorException::getTrace' => ['array'], +'ErrorException::getTrace' => ['list\',args?:list,object?:object}>'], 'ErrorException::getTraceAsString' => ['string'], 'escapeshellarg' => ['string', 'arg'=>'string'], 'escapeshellcmd' => ['string', 'command'=>'string'], @@ -2369,7 +1827,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 +2085,20 @@ 'Exception::getLine' => ['int'], 'Exception::getMessage' => ['string'], 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], -'Exception::getTrace' => ['array'], +'Exception::getTrace' => ['list\',args?:list,object?:object}>'], 'Exception::getTraceAsString' => ['string'], -'exec' => ['string', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], +'exec' => ['string|false', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], 'exif_imagetype' => ['int|false', 'imagefile'=>'string'], 'exif_read_data' => ['array|false', 'filename'=>'string|resource', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'exif_tagname' => ['string|false', 'index'=>'int'], 'exif_thumbnail' => ['string|false', 'filename'=>'string', '&w_width='=>'int', '&w_height='=>'int', '&w_imagetype='=>'int'], -'exit' => ['', 'status'=>'string|int'], 'exp' => ['float', 'number'=>'float'], 'expect_expectl' => ['int', 'expect'=>'resource', 'cases'=>'array', 'match='=>'array'], 'expect_popen' => ['resource|false', 'command'=>'string'], -'explode' => ['non-empty-array|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], +'explode' => ['list|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'number'=>'float'], 'extension_loaded' => ['bool', 'extension_name'=>'string'], -'extract' => ['int', '&rw_var_array'=>'array', 'extract_type='=>'int', 'prefix='=>'string|null'], +'extract' => ['0|positive-int', 'array'=>'array', 'flags='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], @@ -2937,13 +2393,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' => ['non-empty-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 +2414,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'], @@ -2971,10 +2427,10 @@ 'filter_id' => ['int|false', 'filtername'=>'string'], 'filter_input' => ['mixed', 'type'=>'int', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'int', 'definition='=>'int|array', 'add_empty='=>'bool'], -'filter_list' => ['array'], +'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed', 'variable'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], 'filter_var_array' => ['array|false|null', 'data'=>'array', 'definition='=>'mixed', 'add_empty='=>'bool'], -'FilterIterator::__construct' => ['void', 'iterator'=>'iterator'], +'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'FilterIterator::accept' => ['bool'], 'FilterIterator::current' => ['mixed'], 'FilterIterator::getInnerIterator' => ['Iterator'], @@ -2992,31 +2448,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', '...values='=>'string|int|float'], -'fputcsv' => ['int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], -'fputs' => ['int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'int'], -'fread' => ['string|false', 'fp'=>'resource', 'length'=>'int'], +'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|false', '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 +2480,22 @@ 'ftp_connect' => ['resource|false', 'host'=>'string', 'port='=>'int', 'timeout='=>'int'], 'ftp_delete' => ['bool', 'stream'=>'resource', 'file'=>'string'], 'ftp_exec' => ['bool', 'stream'=>'resource', 'command'=>'string'], -'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'int', 'resumepos='=>'int'], -'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'int', 'startpos='=>'int'], -'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'int', 'resume_pos='=>'int'], +'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], +'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], +'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], 'ftp_get_option' => ['mixed', 'stream'=>'resource', 'option'=>'int'], 'ftp_login' => ['bool', 'stream'=>'resource', 'username'=>'string', 'password'=>'string'], 'ftp_mdtm' => ['int', 'stream'=>'resource', 'filename'=>'string'], 'ftp_mkdir' => ['string|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_mlsd' => ['array|false', 'ftp_stream'=>'resource', 'directory'=>'string'], 'ftp_nb_continue' => ['int', 'stream'=>'resource'], -'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode'=>'int', 'resumepos='=>'int'], -'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode'=>'int', 'startpos='=>'int'], -'ftp_nb_get' => ['int', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode'=>'int', 'resume_pos='=>'int'], -'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], +'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], +'ftp_nb_get' => ['int|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], +'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], 'ftp_nlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_pasv' => ['bool', 'stream'=>'resource', 'pasv'=>'bool'], -'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], 'ftp_pwd' => ['string|false', 'stream'=>'resource'], 'ftp_raw' => ['array', 'stream'=>'resource', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string', 'recursive='=>'bool'], @@ -3050,12 +2506,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,47 +2758,47 @@ '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|false', 'extension_name'=>'string'], +'get_declared_classes' => ['list'], +'get_declared_interfaces' => ['list'], +'get_declared_traits' => ['list'], +'get_defined_constants' => ['array', 'categorize='=>'bool'], +'get_defined_functions' => ['array{internal:non-empty-list,user:list}', 'exclude_disabled='=>'bool'], +'get_defined_vars' => ['array'], +'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], 'get_html_translation_table' => ['array', 'table='=>'int', 'flags='=>'int', 'encoding='=>'string'], -'get_include_path' => ['string|false'], -'get_included_files' => ['array'], -'get_loaded_extensions' => ['array', 'zend_extensions='=>'bool'], -'get_magic_quotes_gpc' => ['bool'], -'get_magic_quotes_runtime' => ['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'], +'getimagesize' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'imagefile'=>'string', '&w_info='=>'array'], +'getimagesizefromstring' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'data'=>'string', '&w_info='=>'array'], 'getlastmod' => ['int|false'], 'getmxrr' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight='=>'array'], 'getmygid' => ['int|false'], 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], 'getmyuid' => ['int|false'], -'getopt' => ['array|array|array>|false', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], +'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], 'getprotobyname' => ['int|false', 'name'=>'string'], 'getprotobynumber' => ['string|false', 'proto'=>'int'], 'getrandmax' => ['int'], @@ -3352,7 +2808,7 @@ 'gettext' => ['string', 'msgid'=>'string'], 'gettimeofday' => ['array|float', 'get_as_float='=>'bool'], 'gettype' => ['string', 'var'=>'mixed'], -'glob' => ['array|false', 'pattern'=>'string', 'flags='=>'int'], +'glob' => ['list|false', 'pattern'=>'string', 'flags='=>'int'], 'GlobIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], 'GlobIterator::cont' => ['int'], 'GlobIterator::count' => ['0|positive-int'], @@ -3550,7 +3006,7 @@ 'gmp_clrbit' => ['void', 'a'=>'GMP|string|int', 'index'=>'int'], 'gmp_cmp' => ['int', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_com' => ['GMP', 'a'=>'GMP|string|int'], -'gmp_div' => ['GMP', 'a'=>'GMP|resource|string', 'b'=>'GMP|resource|string', 'round='=>'int'], +'gmp_div' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_q' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_qr' => ['array', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_r' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], @@ -3602,47 +3058,55 @@ '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'], @@ -3728,7 +3192,7 @@ 'gzdeflate' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding='=>'int'], 'gzencode' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding_mode='=>'int'], 'gzeof' => ['bool', 'zp'=>'resource'], -'gzfile' => ['array|false', 'filename'=>'string', 'use_include_path='=>'int'], +'gzfile' => ['list|false', 'filename'=>'string', 'use_include_path='=>'int'], 'gzgetc' => ['string|false', 'zp'=>'resource'], 'gzgets' => ['string|false', 'zp'=>'resource', 'length='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], @@ -3906,18 +3370,18 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], -'hash' => ['string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], -'hash_algos' => ['array'], +'hash' => ['non-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|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], -'hash_hmac_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_file' => ['non-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_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], @@ -3929,7 +3393,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 +3418,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'], @@ -4452,7 +3916,7 @@ 'iconv_mime_decode_headers' => ['array|false', 'headers'=>'string', 'mode='=>'int', 'charset='=>'string'], 'iconv_mime_encode' => ['string|false', 'field_name'=>'string', 'field_value'=>'string', 'preference='=>'array'], 'iconv_set_encoding' => ['bool', 'type'=>'string', 'charset'=>'string'], -'iconv_strlen' => ['int|false', 'str'=>'string', 'charset='=>'string'], +'iconv_strlen' => ['0|positive-int|false', 'str'=>'string', 'charset='=>'string'], 'iconv_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'charset='=>'string'], 'iconv_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'charset='=>'string'], 'iconv_substr' => ['string|false', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'charset='=>'string'], @@ -4507,9 +3971,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'], @@ -4539,21 +4003,21 @@ 'imagebmp' => ['bool', 'image'=>'resource', 'to='=>'string|resource|null', 'compressed='=>'bool'], 'imagechar' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], 'imagecharup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], -'imagecolorallocate' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorallocatealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorat' => ['int|false', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], -'imagecolorclosest' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorclosestalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorclosesthwb' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], +'imagecolorallocate' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorallocatealpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorat' => ['int<0, max>|false', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], +'imagecolorclosest' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorclosestalpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorclosesthwb' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], 'imagecolordeallocate' => ['bool', 'im'=>'resource', 'index'=>'int'], -'imagecolorexact' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorexactalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], +'imagecolorexact' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorexactalpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], 'imagecolormatch' => ['bool', 'im1'=>'resource', 'im2'=>'resource'], -'imagecolorresolve' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorresolvealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorset' => ['void', 'im'=>'resource', 'col'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha='=>'int'], -'imagecolorsforindex' => ['array|false', 'im'=>'resource', 'col'=>'int'], -'imagecolorstotal' => ['int', 'im'=>'resource'], +'imagecolorresolve' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorresolvealpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorset' => ['void', 'im'=>'resource', 'col'=>'int', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha='=>'int<0, 127>'], +'imagecolorsforindex' => ['array{red: int<0, 255>, green: int<0, 255>, blue: int<0, 255>, alpha: int<0, 127>}', 'im'=>'resource', 'col'=>'int'], +'imagecolorstotal' => ['int<0, 256>', 'im'=>'resource'], 'imagecolortransparent' => ['int', 'im'=>'resource', 'col='=>'int'], 'imageconvolution' => ['bool', 'src_im'=>'resource', 'matrix3x3'=>'array', 'div'=>'float', 'offset'=>'float'], 'imagecopy' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'src_w'=>'int', 'src_h'=>'int'], @@ -4561,7 +4025,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,7 +4038,7 @@ 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], -'imagecreatetruecolor' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], +'imagecreatetruecolor' => ['__benevolent', 'x_size'=>'int<1, max>', 'y_size'=>'int<1, max>'], 'imagecrop' => ['resource|false', 'im'=>'resource', 'rect'=>'array'], 'imagecropauto' => ['resource|false', 'im'=>'resource', 'mode='=>'int', 'threshold='=>'float', 'color='=>'int'], 'imagedashedline' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int', 'col'=>'int'], @@ -4634,8 +4098,8 @@ 'imagesettile' => ['bool', 'image'=>'resource', 'tile'=>'resource'], 'imagestring' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], 'imagestringup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], -'imagesx' => ['int', 'im'=>'resource'], -'imagesy' => ['int', 'im'=>'resource'], +'imagesx' => ['int<1, max>', 'im'=>'resource'], +'imagesy' => ['int<1, max>', 'im'=>'resource'], 'imagetruecolortopalette' => ['bool', 'im'=>'resource', 'ditherflag'=>'bool', 'colorswanted'=>'int'], 'imagettfbbox' => ['array|false', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string'], 'imagettftext' => ['array|false', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string'], @@ -4645,25 +4109,25 @@ 'imagexbm' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'foreground='=>'int'], 'Imagick::__construct' => ['void', 'files='=>''], 'Imagick::__toString' => ['string'], -'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::adaptiveResizeImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'bestfit='=>'bool'], -'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::adaptiveThresholdImage' => ['bool', 'width'=>'int', 'height'=>'int', 'offset'=>'int'], 'Imagick::addImage' => ['bool', 'source'=>'imagick'], -'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'int', 'channel='=>'int'], +'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'Imagick::NOISE_*', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::affineTransformImage' => ['bool', 'matrix'=>'imagickdraw'], 'Imagick::animateImages' => ['bool', 'x_server'=>'string'], 'Imagick::annotateImage' => ['bool', 'draw_settings'=>'imagickdraw', 'x'=>'float', 'y'=>'float', 'angle'=>'float', 'text'=>'string'], 'Imagick::appendImages' => ['Imagick', 'stack'=>'bool'], -'Imagick::autoGammaImage' => ['bool', 'channel='=>'int'], -'Imagick::autoLevelImage' => ['bool', 'CHANNEL='=>'string'], +'Imagick::autoGammaImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::autoLevelImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::autoOrient' => ['bool'], 'Imagick::averageImages' => ['Imagick'], 'Imagick::blackThresholdImage' => ['bool', 'threshold'=>'mixed'], 'Imagick::blueShiftImage' => ['bool', 'factor='=>'float'], -'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::borderImage' => ['bool', 'bordercolor'=>'mixed', 'width'=>'int', 'height'=>'int'], -'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'int'], +'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::charcoalImage' => ['bool', 'radius'=>'float', 'sigma'=>'float'], 'Imagick::chopImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::clampImage' => ['bool', 'channel='=>'int'], @@ -4677,16 +4141,16 @@ 'Imagick::colorFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int'], 'Imagick::colorizeImage' => ['bool', 'colorize'=>'mixed', 'opacity'=>'mixed'], 'Imagick::colorMatrixImage' => ['bool', 'color_matrix'=>'array'], -'Imagick::combineImages' => ['Imagick', 'channeltype'=>'int'], +'Imagick::combineImages' => ['Imagick', 'channeltype'=>'Imagick::CHANNEL_*'], 'Imagick::commentImage' => ['bool', 'comment'=>'string'], -'Imagick::compareImageChannels' => ['array', 'image'=>'imagick', 'channeltype'=>'int', 'metrictype'=>'int'], -'Imagick::compareImageLayers' => ['Imagick', 'method'=>'int'], -'Imagick::compareImages' => ['array', 'compare'=>'imagick', 'metric'=>'int'], -'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'int', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], +'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], +'Imagick::compareImageLayers' => ['Imagick', 'method'=>'Imagick::LAYERMETHOD_*'], +'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], +'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'Imagick::COMPOSITE_*', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::compositeImageGravity' => ['bool', 'imagick'=>'Imagick', 'COMPOSITE_CONSTANT'=>'int', 'GRAVITY_CONSTANT'=>'int'], 'Imagick::contrastImage' => ['bool', 'sharpen'=>'bool'], -'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'int'], -'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'int'], +'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::count' => ['0|positive-int', 'mode='=>'int'], 'Imagick::cropImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::cropThumbnailImage' => ['bool', 'width'=>'int', 'height'=>'int', 'legacy='=>'bool'], @@ -4701,167 +4165,167 @@ 'Imagick::destroy' => ['bool'], 'Imagick::displayImage' => ['bool', 'servername'=>'string'], 'Imagick::displayImages' => ['bool', 'servername'=>'string'], -'Imagick::distortImage' => ['bool', 'method'=>'int', 'arguments'=>'array', 'bestfit'=>'bool'], +'Imagick::distortImage' => ['bool', 'method'=>'Imagick::DISTORTION_*', 'arguments'=>'array', 'bestfit'=>'bool'], 'Imagick::drawImage' => ['bool', 'draw'=>'imagickdraw'], 'Imagick::edgeImage' => ['bool', 'radius'=>'float'], 'Imagick::embossImage' => ['bool', 'radius'=>'float', 'sigma'=>'float'], 'Imagick::encipherImage' => ['bool', 'passphrase'=>'string'], 'Imagick::enhanceImage' => ['bool'], 'Imagick::equalizeImage' => ['bool'], -'Imagick::evaluateImage' => ['bool', 'op'=>'int', 'constant'=>'float', 'channel='=>'int'], +'Imagick::evaluateImage' => ['bool', 'op'=>'Imagick::EVALUATE_*', 'constant'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::evaluateImages' => ['bool', 'EVALUATE_CONSTANT'=>'int'], -'Imagick::exportImagePixels' => ['array', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int'], +'Imagick::exportImagePixels' => ['list', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*'], 'Imagick::extentImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::filter' => ['bool', 'ImagickKernel'=>'ImagickKernel', 'CHANNEL='=>'int'], 'Imagick::flattenImages' => ['Imagick'], 'Imagick::flipImage' => ['bool'], -'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'int'], +'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::flopImage' => ['bool'], 'Imagick::forwardFourierTransformimage' => ['bool', 'magnitude'=>'bool'], 'Imagick::frameImage' => ['bool', 'matte_color'=>'mixed', 'width'=>'int', 'height'=>'int', 'inner_bevel'=>'int', 'outer_bevel'=>'int'], -'Imagick::functionImage' => ['bool', 'function'=>'int', 'arguments'=>'array', 'channel='=>'int'], -'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'int'], -'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'int'], -'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], -'Imagick::getColorspace' => ['int'], -'Imagick::getCompression' => ['int'], +'Imagick::functionImage' => ['bool', 'function'=>'Imagick::FUNCTION_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getCompressionQuality' => ['int'], -'Imagick::getConfigureOptions' => ['string'], +'Imagick::getConfigureOptions' => ['array'], 'Imagick::getCopyright' => ['string'], 'Imagick::getFeatures' => ['string'], 'Imagick::getFilename' => ['string'], 'Imagick::getFont' => ['string'], 'Imagick::getFormat' => ['string'], -'Imagick::getGravity' => ['int'], +'Imagick::getGravity' => ['Imagick::GRAVITY_*'], 'Imagick::getHDRIEnabled' => ['int'], 'Imagick::getHomeURL' => ['string'], 'Imagick::getImage' => ['Imagick'], -'Imagick::getImageAlphaChannel' => ['int'], +'Imagick::getImageAlphaChannel' => ['bool'], 'Imagick::getImageArtifact' => ['string', 'artifact'=>'string'], 'Imagick::getImageAttribute' => ['string', 'key'=>'string'], 'Imagick::getImageBackgroundColor' => ['ImagickPixel'], 'Imagick::getImageBlob' => ['string'], -'Imagick::getImageBluePrimary' => ['array'], +'Imagick::getImageBluePrimary' => ['array{x:float,y:float}'], 'Imagick::getImageBorderColor' => ['ImagickPixel'], -'Imagick::getImageChannelDepth' => ['int', 'channel'=>'int'], -'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'int', 'metric'=>'int'], -'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'int', 'channel='=>'int'], -'Imagick::getImageChannelExtrema' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelKurtosis' => ['array', 'channel='=>'int'], -'Imagick::getImageChannelMean' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelRange' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelStatistics' => ['array'], +'Imagick::getImageChannelDepth' => ['int', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'Imagick::CHANNEL_*', 'metric'=>'Imagick::METRIC_*'], +'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'Imagick::METRIC_*', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelExtrema' => ['array{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelStatistics' => ['array{mean:float,minima:float,maxima:float,standardDeviation:float,depth:int}'], 'Imagick::getImageClipMask' => ['Imagick'], 'Imagick::getImageColormapColor' => ['ImagickPixel', 'index'=>'int'], 'Imagick::getImageColors' => ['int'], -'Imagick::getImageColorspace' => ['int'], -'Imagick::getImageCompose' => ['int'], -'Imagick::getImageCompression' => ['int'], +'Imagick::getImageColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getImageCompose' => ['Imagick::COMPOSITE_*'], +'Imagick::getImageCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getImageCompressionQuality' => ['int'], 'Imagick::getImageDelay' => ['int'], 'Imagick::getImageDepth' => ['int'], -'Imagick::getImageDispose' => ['int'], -'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'int'], -'Imagick::getImageExtrema' => ['array'], +'Imagick::getImageDispose' => ['Imagick::DISPOSE_*'], +'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'Imagick::METRIC_*'], +'Imagick::getImageExtrema' => ['array{min:0|positive-int,max:0|positive-int}'], 'Imagick::getImageFilename' => ['string'], 'Imagick::getImageFormat' => ['string'], 'Imagick::getImageGamma' => ['float'], -'Imagick::getImageGeometry' => ['array'], -'Imagick::getImageGravity' => ['int'], -'Imagick::getImageGreenPrimary' => ['array'], +'Imagick::getImageGeometry' => ['array{width:int,height:int}'], +'Imagick::getImageGravity' => ['Imagick::GRAVITY_*'], +'Imagick::getImageGreenPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageHeight' => ['int'], -'Imagick::getImageHistogram' => ['array'], +'Imagick::getImageHistogram' => ['list'], 'Imagick::getImageIndex' => ['int'], -'Imagick::getImageInterlaceScheme' => ['int'], -'Imagick::getImageInterpolateMethod' => ['int'], +'Imagick::getImageInterlaceScheme' => ['Imagick::INTERLACE_*'], +'Imagick::getImageInterpolateMethod' => ['Imagick::INTERPOLATE_*'], 'Imagick::getImageIterations' => ['int'], -'Imagick::getImageLength' => ['int'], +'Imagick::getImageLength' => ['0|positive-int'], 'Imagick::getImageMagickLicense' => ['string'], 'Imagick::getImageMatte' => ['bool'], 'Imagick::getImageMatteColor' => ['ImagickPixel'], -'Imagick::getImageMimeType' => ['string'], -'Imagick::getImageOrientation' => ['int'], -'Imagick::getImagePage' => ['array'], +'Imagick::getImageMimeType' => ['non-empty-string'], +'Imagick::getImageOrientation' => ['Imagick::ORIENTATION_*'], +'Imagick::getImagePage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getImagePixelColor' => ['ImagickPixel', 'x'=>'int', 'y'=>'int'], 'Imagick::getImageProfile' => ['string', 'name'=>'string'], 'Imagick::getImageProfiles' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperties' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperty' => ['string', 'name'=>'string'], -'Imagick::getImageRedPrimary' => ['array'], +'Imagick::getImageRedPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageRegion' => ['Imagick', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], -'Imagick::getImageRenderingIntent' => ['int'], -'Imagick::getImageResolution' => ['array'], +'Imagick::getImageRenderingIntent' => ['Imagick::RENDERINGINTENT_*'], +'Imagick::getImageResolution' => ['array{x:float,y:float}'], 'Imagick::getImagesBlob' => ['string'], -'Imagick::getImageScene' => ['int'], +'Imagick::getImageScene' => ['0|positive-int'], 'Imagick::getImageSignature' => ['string'], -'Imagick::getImageSize' => ['int'], -'Imagick::getImageTicksPerSecond' => ['int'], +'Imagick::getImageSize' => ['0|positive-int'], +'Imagick::getImageTicksPerSecond' => ['0|positive-int'], 'Imagick::getImageTotalInkDensity' => ['float'], -'Imagick::getImageType' => ['int'], +'Imagick::getImageType' => ['Imagick::IMGTYPE_*'], 'Imagick::getImageUnits' => ['int'], 'Imagick::getImageVirtualPixelMethod' => ['int'], -'Imagick::getImageWhitePoint' => ['array'], -'Imagick::getImageWidth' => ['int'], -'Imagick::getInterlaceScheme' => ['int'], +'Imagick::getImageWhitePoint' => ['array{x:float,y:float}'], +'Imagick::getImageWidth' => ['0|positive-int'], +'Imagick::getInterlaceScheme' => ['Imagick::INTERLACE_*'], 'Imagick::getIteratorIndex' => ['int'], -'Imagick::getNumberImages' => ['int'], +'Imagick::getNumberImages' => ['0|positive-int'], 'Imagick::getOption' => ['string', 'key'=>'string'], 'Imagick::getPackageName' => ['string'], -'Imagick::getPage' => ['array'], +'Imagick::getPage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getPixelIterator' => ['ImagickPixelIterator'], 'Imagick::getPixelRegionIterator' => ['ImagickPixelIterator', 'x'=>'int', 'y'=>'int', 'columns'=>'int', 'rows'=>'int'], 'Imagick::getPointSize' => ['float'], -'Imagick::getQuantum' => ['int'], -'Imagick::getQuantumDepth' => ['array'], -'Imagick::getQuantumRange' => ['array'], +'Imagick::getQuantum' => ['0|positive-int'], +'Imagick::getQuantumDepth' => ['array{quantumDepthLong:0|positive-int,quantumDepthString:numeric-string}'], +'Imagick::getQuantumRange' => ['array{quantumRangeLong:0|positive-int,quantumRangeString:numeric-string}'], 'Imagick::getRegistry' => ['string', 'key'=>'string'], 'Imagick::getReleaseDate' => ['string'], -'Imagick::getResource' => ['int', 'type'=>'int'], -'Imagick::getResourceLimit' => ['int', 'type'=>'int'], -'Imagick::getSamplingFactors' => ['array'], -'Imagick::getSize' => ['array'], +'Imagick::getResource' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], +'Imagick::getResourceLimit' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], +'Imagick::getSamplingFactors' => ['list'], +'Imagick::getSize' => ['array{columns:0|positive-int,rows:0|positive-int}'], 'Imagick::getSizeOffset' => ['int'], -'Imagick::getVersion' => ['array'], +'Imagick::getVersion' => ['array{versionNumber:0|positive-int,versionString:non-falsy-string}'], 'Imagick::haldClutImage' => ['bool', 'clut'=>'imagick', 'channel='=>'int'], 'Imagick::hasNextImage' => ['bool'], 'Imagick::hasPreviousImage' => ['bool'], 'Imagick::identifyFormat' => ['string|false', 'embedText'=>'string'], -'Imagick::identifyImage' => ['array', 'appendrawoutput='=>'bool'], +'Imagick::identifyImage' => ['array{imageName:string,mimetype:string,format:string,units:string,colorSpace:string,type:string,compression:string,fileSize:string,geometry:array{width:0|positive-int,height:0|positive-int},resolution:array{x:float,y:float},signature:string}', 'appendrawoutput='=>'bool'], 'Imagick::identifyImageType' => ['int'], 'Imagick::implodeImage' => ['bool', 'radius'=>'float'], -'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int', 'pixels'=>'array'], +'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*', 'pixels'=>'array'], 'Imagick::inverseFourierTransformImage' => ['bool', 'complement'=>'Imagick', 'magnitude'=>'bool'], 'Imagick::key' => ['int|string'], 'Imagick::labelImage' => ['bool', 'label'=>'string'], -'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'int'], +'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::linearStretchImage' => ['bool', 'blackpoint'=>'float', 'whitepoint'=>'float'], 'Imagick::liquidRescaleImage' => ['bool', 'width'=>'int', 'height'=>'int', 'delta_x'=>'float', 'rigidity'=>'float'], -'Imagick::listRegistry' => ['array'], +'Imagick::listRegistry' => ['array'], 'Imagick::localContrastImage' => ['bool', 'radius'=>'float', 'strength'=>'float'], 'Imagick::magnifyImage' => ['bool'], 'Imagick::mapImage' => ['bool', 'map'=>'imagick', 'dither'=>'bool'], 'Imagick::matteFloodfillImage' => ['bool', 'alpha'=>'float', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int'], 'Imagick::medianFilterImage' => ['bool', 'radius'=>'float'], -'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'int'], +'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'Imagick::LAYERMETHOD_*'], 'Imagick::minifyImage' => ['bool'], 'Imagick::modulateImage' => ['bool', 'brightness'=>'float', 'saturation'=>'float', 'hue'=>'float'], -'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'int', 'frame'=>'string'], +'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'Imagick::MONTAGEMODE_*', 'frame'=>'string'], 'Imagick::morphImages' => ['Imagick', 'number_frames'=>'int'], -'Imagick::morphology' => ['bool', 'morphologyMethod'=>'int', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'int'], +'Imagick::morphology' => ['bool', 'morphologyMethod'=>'Imagick::MORPHOLOGY_*', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::mosaicImages' => ['Imagick'], -'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'int'], -'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'int'], +'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::newImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'background'=>'mixed', 'format='=>'string'], 'Imagick::newPseudoImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'pseudostring'=>'string'], 'Imagick::next' => ['void'], 'Imagick::nextImage' => ['bool'], -'Imagick::normalizeImage' => ['bool', 'channel='=>'int'], +'Imagick::normalizeImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::oilPaintImage' => ['bool', 'radius'=>'float'], -'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'int'], +'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::optimizeImageLayers' => ['bool'], -'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'int'], -'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], -'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'int'], +'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::paintTransparentImage' => ['bool', 'target'=>'mixed', 'alpha'=>'float', 'fuzz'=>'float'], 'Imagick::pingImage' => ['bool', 'filename'=>'string'], 'Imagick::pingImageBlob' => ['bool', 'image'=>'string'], @@ -4870,22 +4334,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,28 +4360,28 @@ 'Imagick::rewind' => ['void'], 'Imagick::rollImage' => ['bool', 'x'=>'int', 'y'=>'int'], 'Imagick::rotateImage' => ['bool', 'background'=>'mixed', 'degrees'=>'float'], -'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'int'], +'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::roundCorners' => ['bool', 'x_rounding'=>'float', 'y_rounding'=>'float', 'stroke_width='=>'float', 'displace='=>'float', 'size_correction='=>'float'], -'Imagick::roundCornersImage' => ['', 'xRounding'=>'', 'yRounding'=>'', 'strokeWidth'=>'', 'displace'=>'', 'sizeCorrection'=>''], +'Imagick::roundCornersImage' => ['bool', 'x_rounding'=>'', 'y_rounding'=>'', 'stroke_width='=>'', 'displace='=>'', 'size_correction='=>''], 'Imagick::sampleImage' => ['bool', 'columns'=>'int', 'rows'=>'int'], -'Imagick::scaleImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'bestfit='=>'bool'], -'Imagick::segmentImage' => ['bool', 'colorspace'=>'int', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], -'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'int'], -'Imagick::separateImageChannel' => ['bool', 'channel'=>'int'], +'Imagick::scaleImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'bestfit='=>'bool', 'legacy='=>'bool'], +'Imagick::segmentImage' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], +'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::separateImageChannel' => ['bool', 'channel'=>'Imagick::CHANNEL_*'], 'Imagick::sepiaToneImage' => ['bool', 'threshold'=>'float'], 'Imagick::setAntiAlias' => ['int', 'antialias'=>'bool'], 'Imagick::setBackgroundColor' => ['bool', 'background'=>'mixed'], -'Imagick::setColorspace' => ['bool', 'colorspace'=>'int'], -'Imagick::setCompression' => ['bool', 'compression'=>'int'], +'Imagick::setColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], +'Imagick::setCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], 'Imagick::setCompressionQuality' => ['bool', 'quality'=>'int'], 'Imagick::setFilename' => ['bool', 'filename'=>'string'], 'Imagick::setFirstIterator' => ['bool'], 'Imagick::setFont' => ['bool', 'font'=>'string'], 'Imagick::setFormat' => ['bool', 'format'=>'string'], -'Imagick::setGravity' => ['bool', 'gravity'=>'int'], +'Imagick::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], 'Imagick::setImage' => ['bool', 'replace'=>'imagick'], 'Imagick::setImageAlpha' => ['bool', 'alpha'=>'float'], -'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'int'], +'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'Imagick::ALPHACHANNEL_*'], 'Imagick::setImageArtifact' => ['bool', 'artifact'=>'string', 'value'=>'string'], 'Imagick::setImageAttribute' => ['bool', 'key'=>'string', 'value'=>'string'], 'Imagick::setImageBackgroundColor' => ['bool', 'background'=>'mixed'], @@ -4925,41 +4389,41 @@ 'Imagick::setImageBiasQuantum' => ['void', 'bias'=>'string'], 'Imagick::setImageBluePrimary' => ['bool', 'x'=>'float', 'y'=>'float'], 'Imagick::setImageBorderColor' => ['bool', 'border'=>'mixed'], -'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'int', 'depth'=>'int'], -'Imagick::setImageChannelMask' => ['', 'channel'=>'int'], +'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'Imagick::CHANNEL_*', 'depth'=>'int'], +'Imagick::setImageChannelMask' => ['', 'channel'=>'Imagick::CHANNEL_*'], 'Imagick::setImageClipMask' => ['bool', 'clip_mask'=>'imagick'], 'Imagick::setImageColormapColor' => ['bool', 'index'=>'int', 'color'=>'imagickpixel'], -'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'int'], -'Imagick::setImageCompose' => ['bool', 'compose'=>'int'], -'Imagick::setImageCompression' => ['bool', 'compression'=>'int'], +'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], +'Imagick::setImageCompose' => ['bool', 'compose'=>'Imagick::COMPOSITE_*'], +'Imagick::setImageCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], 'Imagick::setImageCompressionQuality' => ['bool', 'quality'=>'int'], 'Imagick::setImageDelay' => ['bool', 'delay'=>'int'], 'Imagick::setImageDepth' => ['bool', 'depth'=>'int'], -'Imagick::setImageDispose' => ['bool', 'dispose'=>'int'], +'Imagick::setImageDispose' => ['bool', 'dispose'=>'Imagick::DISPOSE_*'], 'Imagick::setImageExtent' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::setImageFilename' => ['bool', 'filename'=>'string'], 'Imagick::setImageFormat' => ['bool', 'format'=>'string'], 'Imagick::setImageGamma' => ['bool', 'gamma'=>'float'], -'Imagick::setImageGravity' => ['bool', 'gravity'=>'int'], +'Imagick::setImageGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], 'Imagick::setImageGreenPrimary' => ['bool', 'x'=>'float', 'y'=>'float'], 'Imagick::setImageIndex' => ['bool', 'index'=>'int'], -'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'int'], -'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'int'], +'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'Imagick::INTERLACE_*'], +'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'Imagick::INTERPOLATE_*'], 'Imagick::setImageIterations' => ['bool', 'iterations'=>'int'], 'Imagick::setImageMatte' => ['bool', 'matte'=>'bool'], 'Imagick::setImageMatteColor' => ['bool', 'matte'=>'mixed'], 'Imagick::setImageOpacity' => ['bool', 'opacity'=>'float'], -'Imagick::setImageOrientation' => ['bool', 'orientation'=>'int'], +'Imagick::setImageOrientation' => ['bool', 'orientation'=>'Imagick::ORIENTATION_*'], 'Imagick::setImagePage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::setImageProfile' => ['bool', 'name'=>'string', 'profile'=>'string'], 'Imagick::setImageProgressMonitor' => ['', 'filename'=>''], 'Imagick::setImageProperty' => ['bool', 'name'=>'string', 'value'=>'string'], 'Imagick::setImageRedPrimary' => ['bool', 'x'=>'float', 'y'=>'float'], -'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'int'], +'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'Imagick::RENDERINGINTENT_*'], 'Imagick::setImageResolution' => ['bool', 'x_resolution'=>'float', 'y_resolution'=>'float'], 'Imagick::setImageScene' => ['bool', 'scene'=>'int'], 'Imagick::setImageTicksPerSecond' => ['bool', 'ticks_per_second'=>'int'], -'Imagick::setImageType' => ['bool', 'image_type'=>'int'], +'Imagick::setImageType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], 'Imagick::setImageUnits' => ['bool', 'units'=>'int'], 'Imagick::setImageVirtualPixelMethod' => ['bool', 'method'=>'int'], 'Imagick::setImageWhitePoint' => ['bool', 'x'=>'float', 'y'=>'float'], @@ -4976,46 +4440,46 @@ 'Imagick::setSamplingFactors' => ['bool', 'factors'=>'array'], 'Imagick::setSize' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::setSizeOffset' => ['bool', 'columns'=>'int', 'rows'=>'int', 'offset'=>'int'], -'Imagick::setType' => ['bool', 'image_type'=>'int'], +'Imagick::setType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], 'Imagick::shadeImage' => ['bool', 'gray'=>'bool', 'azimuth'=>'float', 'elevation'=>'float'], 'Imagick::shadowImage' => ['bool', 'opacity'=>'float', 'sigma'=>'float', 'x'=>'int', 'y'=>'int'], -'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::shaveImage' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::shearImage' => ['bool', 'background'=>'mixed', 'x_shear'=>'float', 'y_shear'=>'float'], -'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'int'], -'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'int'], +'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'Imagick::METRIC_*'], 'Imagick::sketchImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float'], 'Imagick::smushImages' => ['Imagick', 'stack'=>'bool', 'offset'=>'int'], -'Imagick::solarizeImage' => ['bool', 'threshold'=>'int'], -'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'int', 'arguments'=>'array', 'channel='=>'int'], +'Imagick::solarizeImage' => ['bool', 'threshold'=>'0|positive-int'], +'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'Imagick::SPARSECOLORMETHOD_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::spliceImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::spreadImage' => ['bool', 'radius'=>'float'], -'Imagick::statisticImage' => ['bool', 'type'=>'int', 'width'=>'int', 'height'=>'int', 'channel='=>'int'], +'Imagick::statisticImage' => ['bool', 'type'=>'Imagick::STATISTIC_*', 'width'=>'int', 'height'=>'int', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::steganoImage' => ['Imagick', 'watermark_wand'=>'imagick', 'offset'=>'int'], 'Imagick::stereoImage' => ['bool', 'offset_wand'=>'imagick'], 'Imagick::stripImage' => ['bool'], 'Imagick::subImageMatch' => ['Imagick', 'Imagick'=>'Imagick', '&w_offset='=>'array', '&w_similarity='=>'float'], 'Imagick::swirlImage' => ['bool', 'degrees'=>'float'], 'Imagick::textureImage' => ['Imagick', 'texture_wand'=>'imagick'], -'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'int'], +'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::thumbnailImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'bestfit='=>'bool', 'fill='=>'bool', 'legacy='=>'bool'], 'Imagick::tintImage' => ['bool', 'tint'=>'mixed', 'opacity'=>'mixed'], 'Imagick::transformImage' => ['Imagick', 'crop'=>'string', 'geometry'=>'string'], -'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'int'], +'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], 'Imagick::transparentPaintImage' => ['bool', 'target'=>'mixed', 'alpha'=>'float', 'fuzz'=>'float', 'invert'=>'bool'], 'Imagick::transposeImage' => ['bool'], 'Imagick::transverseImage' => ['bool'], 'Imagick::trimImage' => ['bool', 'fuzz'=>'float'], 'Imagick::uniqueImageColors' => ['bool'], -'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'int'], +'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::valid' => ['bool'], 'Imagick::vignetteImage' => ['bool', 'blackpoint'=>'float', 'whitepoint'=>'float', 'x'=>'int', 'y'=>'int'], 'Imagick::waveImage' => ['bool', 'amplitude'=>'float', 'length'=>'float'], 'Imagick::whiteThresholdImage' => ['bool', 'threshold'=>'mixed'], 'Imagick::writeImage' => ['bool', 'filename='=>'string'], -'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'Imagick::writeImages' => ['bool', 'filename'=>'string', 'adjoin'=>'bool'], -'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'ImagickDraw::__construct' => ['void'], 'ImagickDraw::affine' => ['bool', 'affine'=>'array'], 'ImagickDraw::annotation' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], @@ -5024,9 +4488,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 +4500,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 +4530,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 +4571,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 +4594,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 +4616,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 +4676,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 +4711,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 +4733,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 +4747,7 @@ 'inclued_get_data' => ['array'], 'inet_ntop' => ['string|false', 'in_addr'=>'string'], 'inet_pton' => ['string|false', 'ip_address'=>'string'], -'InfiniteIterator::__construct' => ['void', 'iterator'=>'iterator'], +'InfiniteIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'InfiniteIterator::next' => ['void'], 'inflate_add' => ['string|false', 'context'=>'resource', 'encoded_data'=>'string', 'flush_mode='=>'int'], 'inflate_get_read_len' => ['int|false', 'resource'=>'resource'], @@ -5329,10 +4792,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 +4816,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'], @@ -5513,24 +4976,24 @@ 'IntlChar::toupper' => ['mixed', 'codepoint'=>'mixed'], 'IntlCodePointBreakIterator::getLastCodePoint' => ['int'], 'IntlDateFormatter::__construct' => ['void', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'null|int|IntlCalendar', 'pattern='=>'string'], -'IntlDateFormatter::create' => ['IntlDateFormatter|false', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'int|IntlCalendar', 'pattern='=>'string'], +'IntlDateFormatter::create' => ['IntlDateFormatter|null', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'int|IntlCalendar', 'pattern='=>'string'], 'IntlDateFormatter::format' => ['string|false', 'args'=>''], -'IntlDateFormatter::formatObject' => ['string', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], -'IntlDateFormatter::getCalendar' => ['int'], -'IntlDateFormatter::getCalendarObject' => ['IntlCalendar'], -'IntlDateFormatter::getDateType' => ['int'], +'IntlDateFormatter::formatObject' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], +'IntlDateFormatter::getCalendar' => ['int|false'], +'IntlDateFormatter::getCalendarObject' => ['IntlCalendar|false|null'], +'IntlDateFormatter::getDateType' => ['int|false'], 'IntlDateFormatter::getErrorCode' => ['int'], 'IntlDateFormatter::getErrorMessage' => ['string'], -'IntlDateFormatter::getLocale' => ['string'], -'IntlDateFormatter::getPattern' => ['string'], -'IntlDateFormatter::getTimeType' => ['int'], -'IntlDateFormatter::getTimeZone' => ['IntlTimeZone'], -'IntlDateFormatter::getTimeZoneId' => ['string'], +'IntlDateFormatter::getLocale' => ['string|false'], +'IntlDateFormatter::getPattern' => ['string|false'], +'IntlDateFormatter::getTimeType' => ['int|false'], +'IntlDateFormatter::getTimeZone' => ['IntlTimeZone|false'], +'IntlDateFormatter::getTimeZoneId' => ['string|false'], 'IntlDateFormatter::isLenient' => ['bool'], -'IntlDateFormatter::localtime' => ['array', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], -'IntlDateFormatter::parse' => ['int|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::localtime' => ['array|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::parse' => ['int|float|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], 'IntlDateFormatter::setCalendar' => ['bool', 'calendar'=>''], -'IntlDateFormatter::setLenient' => ['bool', 'lenient'=>'bool'], +'IntlDateFormatter::setLenient' => ['void', 'lenient'=>'bool'], 'IntlDateFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'IntlDateFormatter::setTimeZone' => ['bool', 'timezone'=>''], 'IntlDateFormatter::setTimeZoneId' => ['bool', 'zone'=>'string', 'fmt='=>'IntlDateFormatter'], @@ -5542,6 +5005,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'], @@ -5611,7 +5075,7 @@ 'intltz_to_date_time_zone' => ['DateTimeZone|false', 'obj'=>''], 'intltz_use_daylight_time' => ['bool', 'obj'=>''], 'intlz_create_default' => ['IntlTimeZone'], -'intval' => ['int', 'var'=>'mixed', 'base='=>'int'], +'intval' => ['int', 'var'=>'scalar|array|resource|null', 'base='=>'int'], 'InvalidArgumentException::__clone' => ['void'], 'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?InvalidArgumentException)'], 'InvalidArgumentException::__toString' => ['string'], @@ -5620,11 +5084,11 @@ 'InvalidArgumentException::getLine' => ['int'], 'InvalidArgumentException::getMessage' => ['string'], 'InvalidArgumentException::getPrevious' => ['Throwable|InvalidArgumentException|null'], -'InvalidArgumentException::getTrace' => ['array'], +'InvalidArgumentException::getTrace' => ['list\',args?:list,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,7 +5121,6 @@ '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'], @@ -5693,10 +5156,11 @@ 'jobqueue_license_info' => ['array'], 'join' => ['string', 'glue'=>'string', 'pieces'=>'array'], 'join\'1' => ['string', 'pieces'=>'array'], +'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], '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'=>''], @@ -5848,8 +5312,8 @@ 'ldap_8859_to_t61' => ['string|false', 'value'=>'string'], 'ldap_add' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_add_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], -'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], -'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], +'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], +'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], 'ldap_close' => ['bool', 'link_identifier'=>'resource'], 'ldap_compare' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'attr'=>'string', 'value'=>'string', 'servercontrols='=>'array'], 'ldap_connect' => ['resource|false', 'host='=>'string', 'port='=>'int', 'wallet='=>'string', 'wallet_passwd='=>'string', 'authmode='=>'int'], @@ -5899,7 +5363,7 @@ 'ldap_sasl_bind' => ['bool', 'link_identifier'=>'resource', 'binddn='=>'string', 'password='=>'string', 'sasl_mech='=>'string', 'sasl_realm='=>'string', 'sasl_authc_id='=>'string', 'sasl_authz_id='=>'string', 'props='=>'string'], 'ldap_search' => ['resource|false', 'link_identifier'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'servercontrols='=>'array'], 'ldap_set_option' => ['bool', 'link_identifier'=>'resource|null', 'option'=>'int', 'newval'=>'mixed'], -'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'string'], +'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'callable'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], 'ldap_start_tls' => ['bool', 'link_identifier'=>'resource'], 'ldap_t61_to_8859' => ['string|false', 'value'=>'string'], @@ -5918,13 +5382,13 @@ 'LengthException::getLine' => ['int'], 'LengthException::getMessage' => ['string'], 'LengthException::getPrevious' => ['Throwable|LengthException|null'], -'LengthException::getTrace' => ['array'], +'LengthException::getTrace' => ['list\',args?:list,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'], 'libxml_clear_errors' => ['void'], 'libxml_disable_entity_loader' => ['bool', 'disable='=>'bool'], -'libxml_get_errors' => ['array'], +'libxml_get_errors' => ['list'], 'libxml_get_last_error' => ['LibXMLError|false'], 'libxml_set_external_entity_loader' => ['bool', 'resolver_function'=>'callable'], 'libxml_set_streams_context' => ['void', 'streams_context'=>'resource'], @@ -5949,41 +5413,41 @@ 'linkinfo' => ['int|false', 'filename'=>'string'], 'litespeed_request_headers' => ['array'], 'litespeed_response_headers' => ['array|false'], -'Locale::acceptFromHttp' => ['string|false', 'header'=>'string'], -'Locale::canonicalize' => ['string', 'locale'=>'string'], -'Locale::composeLocale' => ['string', 'subtags'=>'array'], -'Locale::filterMatches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'Locale::getAllVariants' => ['array', 'locale'=>'string'], -'Locale::getDefault' => ['string'], -'Locale::getDisplayLanguage' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayName' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayRegion' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getKeywords' => ['array|false', 'locale'=>'string'], -'Locale::getPrimaryLanguage' => ['string', 'locale'=>'string'], -'Locale::getRegion' => ['string', 'locale'=>'string'], -'Locale::getScript' => ['string', 'locale'=>'string'], -'Locale::lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'Locale::parseLocale' => ['array', 'locale'=>'string'], +'Locale::acceptFromHttp' => ['non-empty-string|false', 'header'=>'string'], +'Locale::canonicalize' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::composeLocale' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], +'Locale::filterMatches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'Locale::getAllVariants' => ['array|null', 'locale'=>'string'], +'Locale::getDefault' => ['non-empty-string'], +'Locale::getDisplayLanguage' => ['non-empty-string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayName' => ['non-empty-string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayRegion' => ['string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayScript' => ['string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayVariant' => ['string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getKeywords' => ['array|null', 'locale'=>'string'], +'Locale::getPrimaryLanguage' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::getRegion' => ['string|null', 'locale'=>'string'], +'Locale::getScript' => ['string|null', 'locale'=>'string'], +'Locale::lookup' => ['string|null', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'Locale::parseLocale' => ['array|null', 'locale'=>'string'], 'Locale::setDefault' => ['bool', 'locale'=>'string'], -'locale_accept_from_http' => ['string|false', 'header'=>'string'], -'locale_canonicalize' => ['', 'arg1'=>''], -'locale_compose' => ['string|false', 'subtags'=>'array'], -'locale_filter_matches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'locale_get_all_variants' => ['array', 'locale'=>'string'], -'locale_get_default' => ['string'], -'locale_get_display_language' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_name' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_region' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_script' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_variant' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_keywords' => ['array|false', 'locale'=>'string'], -'locale_get_primary_language' => ['string', 'locale'=>'string'], -'locale_get_region' => ['string', 'locale'=>'string'], -'locale_get_script' => ['string', 'locale'=>'string'], -'locale_lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'locale_parse' => ['array', 'locale'=>'string'], +'locale_accept_from_http' => ['non-empty-string|false', 'header'=>'string'], +'locale_canonicalize' => ['non-empty-string|null', 'locale'=>'string'], +'locale_compose' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], +'locale_filter_matches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'locale_get_all_variants' => ['array|null', 'locale'=>'string'], +'locale_get_default' => ['non-empty-string'], +'locale_get_display_language' => ['non-empty-string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_name' => ['non-empty-string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_region' => ['string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_script' => ['string|false', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_variant' => ['string|false', '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,7 +5462,7 @@ 'LogicException::getLine' => ['int'], 'LogicException::getMessage' => ['string'], 'LogicException::getPrevious' => ['Throwable|LogicException|null'], -'LogicException::getTrace' => ['array'], +'LogicException::getTrace' => ['list\',args?:list,object?:object}>'], 'LogicException::getTraceAsString' => ['string'], 'long2ip' => ['string|false', 'proper_address'=>'int'], 'lstat' => ['array|false', 'filename'=>'string'], @@ -6062,7 +5526,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'], @@ -6140,7 +5604,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 +5771,24 @@ 'maxdb_thread_safe' => ['bool'], 'maxdb_use_result' => ['resource', 'link'=>''], 'maxdb_warning_count' => ['int', 'link'=>'resource'], -'mb_check_encoding' => ['bool', 'var='=>'string', 'encoding='=>'string'], +'mb_check_encoding' => ['bool', 'var='=>'string|array', 'encoding='=>'string'], 'mb_chr' => ['string|false', 'cp'=>'int', 'encoding='=>'string'], 'mb_convert_case' => ['string', 'sourcestring'=>'string', 'mode'=>'int', 'encoding='=>'string'], -'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], +'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', 'encoding_list'=>'non-empty-list|non-falsy-string'], +'mb_detect_order\'1' => ['list'], 'mb_encode_mimeheader' => ['string', 'str'=>'string', 'charset='=>'string', 'transfer_encoding='=>'string', 'linefeed='=>'string', 'indent='=>'int'], 'mb_encode_numericentity' => ['string', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string', 'is_hex='=>'bool'], -'mb_encoding_aliases' => ['array|false', 'encoding'=>'string'], +'mb_encoding_aliases' => ['list|false', 'encoding'=>'string'], 'mb_ereg' => ['int|false', 'pattern'=>'string', 'string'=>'string', '&w_registers='=>'array'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'option='=>'string'], 'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], -'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable(array):string', 'string'=>'string', 'option='=>'string'], 'mb_ereg_search' => ['bool', 'pattern='=>'string', 'option='=>'string'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['array|false'], @@ -6338,7 +5803,7 @@ 'mb_http_output' => ['string|bool', 'encoding='=>'string'], 'mb_internal_encoding' => ['string|bool', 'encoding='=>'string'], 'mb_language' => ['string|bool', 'language='=>'string'], -'mb_list_encodings' => ['array'], +'mb_list_encodings' => ['non-empty-list'], 'mb_ord' => ['int|false', 'str'=>'string', 'enc='=>'string'], 'mb_output_handler' => ['string', 'contents'=>'string', 'status'=>'int'], 'mb_parse_str' => ['bool', 'encoded_string'=>'string', '&w_result='=>'array'], @@ -6347,21 +5812,21 @@ 'mb_regex_set_options' => ['string', 'options='=>'string'], 'mb_scrub' => ['string', 'str'=>'string', 'enc='=>'string'], 'mb_send_mail' => ['bool', 'to'=>'string', 'subject'=>'string', 'message'=>'string', 'additional_headers='=>'string|array|null', 'additional_parameter='=>'string'], -'mb_split' => ['array|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], +'mb_split' => ['list|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], 'mb_strcut' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], 'mb_strimwidth' => ['string', 'str'=>'string', 'start'=>'int', 'width'=>'int', 'trimmarker='=>'string', 'encoding='=>'string'], -'mb_stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strlen' => ['int|false', 'str'=>'string', 'encoding='=>'string'], -'mb_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strlen' => ['0|positive-int|false', 'str'=>'string', 'encoding='=>'string'], +'mb_strpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], 'mb_strrichr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], -'mb_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strtolower' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strwidth' => ['int', 'str'=>'string', 'encoding='=>'string'], +'mb_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' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'encoding='=>'string'], @@ -6401,8 +5866,8 @@ 'mcrypt_module_open' => ['resource|false', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], 'mcrypt_module_self_test' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_ofb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], -'md5' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'md5' => ['non-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 +5876,8 @@ 'Memcache::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'Memcache::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'Memcache::flush' => ['bool'], -'Memcache::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'Memcache::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'Memcache::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'Memcache::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'Memcache::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'Memcache::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6478,7 +5944,8 @@ 'MemcachePool::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'MemcachePool::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'MemcachePool::flush' => ['bool'], -'MemcachePool::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'MemcachePool::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'MemcachePool::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'MemcachePool::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'MemcachePool::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'MemcachePool::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6488,8 +5955,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'], @@ -6510,7 +5977,7 @@ 'mhash_keygen_s2k' => ['string|false', 'hash'=>'int', 'input_password'=>'string', 'salt'=>'string', 'bytes'=>'int'], 'microtime' => ['mixed', 'get_as_float='=>'bool'], 'mime_content_type' => ['string|false', 'filename_or_stream'=>'string|resource'], -'min' => ['', '...arg1'=>'array'], +'min' => ['', '...arg1'=>'non-empty-array'], 'min\'1' => ['', 'arg1'=>'', 'arg2'=>'', '...args='=>''], 'ming_keypress' => ['int', 'char'=>'string'], 'ming_setcubicthreshold' => ['void', 'threshold'=>'int'], @@ -6519,7 +5986,7 @@ 'ming_useconstants' => ['void', 'use'=>'int'], 'ming_useswfversion' => ['void', 'version'=>'int'], 'mkdir' => ['bool', 'pathname'=>'string', 'mode='=>'int', 'recursive='=>'bool', 'context='=>'resource'], -'mktime' => ['int|false', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], +'mktime' => ['__benevolent', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'Mongo::__construct' => ['void', 'server='=>'string', 'options='=>'array', 'driver_options='=>'array'], 'Mongo::__get' => ['MongoDB', 'dbname'=>'string'], @@ -6589,7 +6056,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 +6067,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'], @@ -6662,7 +6129,7 @@ 'MongoCursorException::getLine' => ['int'], 'MongoCursorException::getMessage' => ['string'], 'MongoCursorException::getPrevious' => ['Exception|Throwable'], -'MongoCursorException::getTrace' => ['array'], +'MongoCursorException::getTrace' => ['list\',args?:list,object?:object}>'], 'MongoCursorException::getTraceAsString' => ['string'], 'MongoCursorInterface::__construct' => ['void'], 'MongoCursorInterface::batchSize' => ['MongoCursorInterface', 'batchSize'=>'int'], @@ -6708,126 +6175,6 @@ '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\Binary::getData' => ['string'], -'MongoDB\BSON\Binary::getType' => ['int'], -'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\ObjectId::__toString' => ['string'], -'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\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\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], -'MongoDB\Driver\BulkWrite::count' => ['0|positive-int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], -'MongoDB\Driver\BulkWrite::insert' => ['MongoDB\Driver\ObjectID', 'document'=>'array|object'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], -'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], -'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], -'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Cursor::isDead' => ['bool'], -'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], -'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], -'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\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\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\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\ReadPreference::getMode' => ['int'], -'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\Server::getTags' => ['array'], -'MongoDB\Driver\Server::getType' => [''], -'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isDelayed' => [''], -'MongoDB\Driver\Server::isHidden' => ['bool'], -'MongoDB\Driver\Server::isPassive' => [''], -'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\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\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['array', 'db'=>'mongodb', 'ref'=>'array'], 'MongoDBRef::isRef' => ['bool', 'ref'=>'mixed'], @@ -6841,7 +6188,7 @@ 'MongoException::getLine' => ['int'], 'MongoException::getMessage' => ['string'], 'MongoException::getPrevious' => ['Exception|Throwable'], -'MongoException::getTrace' => ['array'], +'MongoException::getTrace' => ['list\',args?:list,object?:object}>'], 'MongoException::getTraceAsString' => ['string'], 'MongoGridFS::__construct' => ['void', 'db'=>'MongoDB', 'prefix='=>'string', 'chunks='=>'mixed'], 'MongoGridFS::__get' => ['MongoCollection', 'name'=>'string'], @@ -6934,7 +6281,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 +6299,7 @@ 'MongoResultException::getLine' => ['int'], 'MongoResultException::getMessage' => ['string'], 'MongoResultException::getPrevious' => ['Exception|Throwable'], -'MongoResultException::getTrace' => ['array'], +'MongoResultException::getTrace' => ['list\',args?:list,object?:object}>'], 'MongoResultException::getTraceAsString' => ['string'], 'MongoTimestamp::__construct' => ['void', 'sec='=>'int', 'inc='=>'int'], 'MongoTimestamp::__toString' => ['string'], @@ -6972,7 +6319,7 @@ 'MongoWriteConcernException::getLine' => ['int'], 'MongoWriteConcernException::getMessage' => ['string'], 'MongoWriteConcernException::getPrevious' => ['Exception|Throwable'], -'MongoWriteConcernException::getTrace' => ['array'], +'MongoWriteConcernException::getTrace' => ['list\',args?:list,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'], @@ -7107,11 +6454,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 +6526,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 +6537,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,7 +6557,7 @@ 'mysqli::store_result' => ['mysqli_result|false', 'option='=>'int'], 'mysqli::thread_safe' => ['bool'], 'mysqli::use_result' => ['mysqli_result|false'], -'mysqli_affected_rows' => ['int', 'link'=>'mysqli'], +'mysqli_affected_rows' => ['int<-1,max>|numeric-string', 'link'=>'mysqli'], 'mysqli_autocommit' => ['bool', 'link'=>'mysqli', 'mode'=>'bool'], 'mysqli_begin_transaction' => ['bool', 'link'=>'mysqli', 'flags='=>'int', 'name='=>'string'], 'mysqli_change_user' => ['bool', 'link'=>'mysqli', 'user'=>'string', 'password'=>'string', 'database'=>'string'], @@ -7234,14 +6581,15 @@ 'mysqli_errno' => ['int', 'link'=>'mysqli'], 'mysqli_error' => ['string|null', 'link'=>'mysqli'], 'mysqli_error_list' => ['array', 'connection'=>'mysqli'], -'mysqli_fetch_all' => ['array|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], +'mysqli_fetch_all' => ['list', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_array' => ['array|null|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], -'mysqli_fetch_assoc' => ['array|null', 'result'=>'mysqli_result'], -'mysqli_fetch_field' => ['object|false', 'result'=>'mysqli_result'], -'mysqli_fetch_field_direct' => ['object|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], -'mysqli_fetch_fields' => ['array|false', 'result'=>'mysqli_result'], +'mysqli_fetch_assoc' => ['array|null|false', 'result'=>'mysqli_result'], +'mysqli_fetch_column' => ['null|int|float|string|false', 'result' => 'mysqli_result', 'column'=>'int'], +'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], +'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], +'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], 'mysqli_fetch_lengths' => ['array|false', 'result'=>'mysqli_result'], -'mysqli_fetch_object' => ['object|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], +'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], 'mysqli_fetch_row' => ['array|null', 'result'=>'mysqli_result'], 'mysqli_field_count' => ['int', 'link'=>'mysqli'], 'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'fieldnr'=>'int'], @@ -7269,13 +6617,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 +6633,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'], @@ -7322,16 +6671,16 @@ 'mysqli_stmt::fetch' => ['bool|null'], 'mysqli_stmt::free_result' => ['void'], 'mysqli_stmt::get_result' => ['mysqli_result|false'], -'mysqli_stmt::get_warnings' => ['object'], +'mysqli_stmt::get_warnings' => ['mysqli_warning|false'], 'mysqli_stmt::more_results' => ['bool'], 'mysqli_stmt::next_result' => ['bool'], -'mysqli_stmt::num_rows' => ['int'], +'mysqli_stmt::num_rows' => ['int<0,max>|numeric-string'], 'mysqli_stmt::prepare' => ['bool', 'query'=>'string'], 'mysqli_stmt::reset' => ['bool'], 'mysqli_stmt::result_metadata' => ['mysqli_result|false'], 'mysqli_stmt::send_long_data' => ['bool', 'param_nr'=>'int', 'data'=>'string'], 'mysqli_stmt::store_result' => ['bool'], -'mysqli_stmt_affected_rows' => ['int|string', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_affected_rows' => ['int<-1,max>|numeric-string', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_attr_get' => ['int|false', 'stmt'=>'mysqli_stmt', 'attr'=>'int'], 'mysqli_stmt_attr_set' => ['bool', 'stmt'=>'mysqli_stmt', 'attr'=>'int', 'mode'=>'int'], 'mysqli_stmt_bind_param' => ['bool', 'stmt'=>'mysqli_stmt', 'types'=>'string', 'var1'=>'mixed', '...args='=>'mixed'], @@ -7340,24 +6689,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|null', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_field_count' => ['int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_field_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_free_result' => ['void', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_get_result' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_get_warnings' => ['object|false', 'stmt'=>'mysqli_stmt'], +'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'], @@ -7754,9 +7103,9 @@ 'ngettext' => ['string', 'msgid1'=>'string', 'msgid2'=>'string', 'n'=>'int'], 'nl2br' => ['string', 'str'=>'string', 'is_xhtml='=>'bool'], 'nl_langinfo' => ['string|false', 'item'=>'int'], -'NoRewindIterator::__construct' => ['void', 'iterator'=>'iterator'], +'NoRewindIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'NoRewindIterator::current' => ['mixed'], -'NoRewindIterator::getInnerIterator' => ['iterator'], +'NoRewindIterator::getInnerIterator' => ['Iterator'], 'NoRewindIterator::key' => ['mixed'], 'NoRewindIterator::next' => ['void'], 'NoRewindIterator::rewind' => ['void'], @@ -7785,11 +7134,11 @@ 'nsapi_response_headers' => ['array'], 'nsapi_virtual' => ['bool', 'uri'=>'string'], 'nthmac' => ['string', 'clent'=>'string', 'data'=>'string'], -'number_format' => ['string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], +'number_format' => ['non-empty-string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], -'NumberFormatter::format' => ['string', 'num'=>'', 'type='=>'int'], -'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], +'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], +'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], @@ -7798,7 +7147,7 @@ 'NumberFormatter::getSymbol' => ['string', 'attr'=>'int'], 'NumberFormatter::getTextAttribute' => ['string', 'attr'=>'int'], 'NumberFormatter::parse' => ['float|false', 'str'=>'string', 'type='=>'int', '&rw_position='=>'int'], -'NumberFormatter::parseCurrency' => ['float', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], +'NumberFormatter::parseCurrency' => ['float|false', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], 'NumberFormatter::setAttribute' => ['bool', 'attr'=>'int', 'value'=>''], 'NumberFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'NumberFormatter::setSymbol' => ['bool', 'attr'=>'int', 'symbol'=>'string'], @@ -7879,7 +7228,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'], @@ -7976,8 +7325,8 @@ 'oci_new_connect' => ['resource|false', 'user'=>'string', 'pass'=>'string', 'db='=>'string', 'charset='=>'string', 'session_mode='=>'int'], 'oci_new_cursor' => ['resource|false', 'connection'=>'resource'], 'oci_new_descriptor' => ['OCI-Lob|false', 'connection'=>'resource', 'type='=>'int'], -'oci_num_fields' => ['int|false', 'stmt'=>'resource'], -'oci_num_rows' => ['int|false', 'stmt'=>'resource'], +'oci_num_fields' => ['0|positive-int|false', 'stmt'=>'resource'], +'oci_num_rows' => ['0|positive-int|false', 'stmt'=>'resource'], 'oci_parse' => ['resource|false', 'connection'=>'resource', 'statement'=>'string'], 'oci_password_change' => ['bool', 'connection'=>'', 'username'=>'string', 'old_password'=>'string', 'new_password'=>'string'], 'oci_pconnect' => ['resource|false', 'user'=>'string', 'pass'=>'string', 'db='=>'string', 'charset='=>'string', 'session_mode='=>'int'], @@ -7997,18 +7346,18 @@ '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'], 'odbc_close_all' => ['void'], -'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_columnprivileges' => ['resource|false', 'connection_id'=>'resource', 'catalog'=>'string', 'schema'=>'string', 'table'=>'string', 'column'=>'string'], +'odbc_columns' => ['resource|false', 'connection_id'=>'resource', 'qualifier='=>'string', 'owner='=>'string', 'table_name='=>'string', 'column_name='=>'string'], 'odbc_commit' => ['bool', 'connection_id'=>'resource'], '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_do' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'odbc_error' => ['string', 'connection_id='=>'resource'], 'odbc_errormsg' => ['string', 'connection_id='=>'resource'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], @@ -8023,26 +7372,26 @@ '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_foreignkeys' => ['resource|false', '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'], +'odbc_gettypeinfo' => ['resource|false', 'connection_id'=>'resource', 'data_type='=>'int'], 'odbc_longreadlen' => ['bool', 'result_id'=>'resource', 'length'=>'int'], 'odbc_next_result' => ['bool', 'result_id'=>'resource'], '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_pconnect' => ['resource|false', 'dsn'=>'string', 'user'=>'string', 'password'=>'string', 'cursor_option='=>'int'], '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_primarykeys' => ['resource|false', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'table'=>'string'], +'odbc_procedurecolumns' => ['resource|false', 'connection_id'=>'', 'qualifier'=>'string', 'owner'=>'string', 'proc'=>'string', 'column'=>'string'], +'odbc_procedures' => ['resource|false', 'connection_id'=>'', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string'], 'odbc_result' => ['mixed', 'result_id'=>'resource', 'field'=>'mixed'], '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'], -'odbc_statistics' => ['resource', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string', 'unique'=>'int', 'accuracy'=>'int'], -'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'], +'odbc_specialcolumns' => ['resource|false', 'connection_id'=>'resource', 'type'=>'int', 'qualifier'=>'string', 'owner'=>'string', 'table'=>'string', 'scope'=>'int', 'nullable'=>'int'], +'odbc_statistics' => ['resource|false', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string', 'unique'=>'int', 'accuracy'=>'int'], +'odbc_tableprivileges' => ['resource|false', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string'], +'odbc_tables' => ['resource|false', 'connection_id'=>'resource', 'qualifier='=>'string', 'owner='=>'string', 'name='=>'string', 'table_types='=>'string'], 'opcache_compile_file' => ['bool', 'file'=>'string'], 'opcache_get_configuration' => ['array|false'], 'opcache_get_status' => ['array|false', 'get_scripts='=>'bool'], @@ -8087,10 +7436,10 @@ 'openssl_encrypt' => ['string|false', 'data'=>'string', 'method'=>'string', 'key'=>'string', 'options='=>'int', 'iv='=>'string', '&w_tag='=>'string', 'aad='=>'string', 'tag_length='=>'int'], 'openssl_error_string' => ['string|false'], 'openssl_free_key' => ['void', 'key_identifier'=>'resource'], -'openssl_get_cert_locations' => ['array'], -'openssl_get_cipher_methods' => ['array', 'aliases='=>'bool'], -'openssl_get_curve_names' => ['array|false'], -'openssl_get_md_methods' => ['array', 'aliases='=>'bool'], +'openssl_get_cert_locations' => ['array'], +'openssl_get_cipher_methods' => ['list', 'aliases='=>'bool'], +'openssl_get_curve_names' => ['list|false'], +'openssl_get_md_methods' => ['list', 'aliases='=>'bool'], 'openssl_get_privatekey' => ['resource|false', 'key'=>'string', 'passphrase='=>'string'], 'openssl_get_publickey' => ['resource|false', 'cert'=>'resource|string'], 'openssl_open' => ['bool', 'sealed_data'=>'string', '&w_open_data'=>'string', 'env_key'=>'string', 'priv_key_id'=>'string|array|resource', 'method='=>'string', 'iv='=>'string'], @@ -8131,7 +7480,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)'], @@ -8141,7 +7490,7 @@ 'OutOfBoundsException::getLine' => ['int'], 'OutOfBoundsException::getMessage' => ['string'], 'OutOfBoundsException::getPrevious' => ['Throwable|OutOfBoundsException|null'], -'OutOfBoundsException::getTrace' => ['array'], +'OutOfBoundsException::getTrace' => ['list\',args?:list,object?:object}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], 'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfRangeException)'], @@ -8151,7 +7500,7 @@ 'OutOfRangeException::getLine' => ['int'], 'OutOfRangeException::getMessage' => ['string'], 'OutOfRangeException::getPrevious' => ['Throwable|OutOfRangeException|null'], -'OutOfRangeException::getTrace' => ['array'], +'OutOfRangeException::getTrace' => ['list\',args?:list,object?:object}>'], 'OutOfRangeException::getTraceAsString' => ['string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_reset_rewrite_vars' => ['bool'], @@ -8163,12 +7512,12 @@ 'OverflowException::getLine' => ['int'], 'OverflowException::getMessage' => ['string'], 'OverflowException::getPrevious' => ['Throwable|OverflowException|null'], -'OverflowException::getTrace' => ['array'], +'OverflowException::getTrace' => ['list\',args?:list,object?:object}>'], 'OverflowException::getTraceAsString' => ['string'], 'overload' => ['', 'class_name'=>'string'], 'override_function' => ['bool', 'function_name'=>'string', 'function_args'=>'string', 'function_code'=>'string'], 'pack' => ['string', 'format'=>'string', '...args='=>'mixed'], -'ParentIterator::__construct' => ['void', 'iterator'=>'recursiveiterator'], +'ParentIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'ParentIterator::accept' => ['bool'], 'ParentIterator::getChildren' => ['ParentIterator'], 'ParentIterator::hasChildren' => ['bool'], @@ -8239,14 +7588,14 @@ 'ParseError::getLine' => ['int'], 'ParseError::getMessage' => ['string'], 'ParseError::getPrevious' => ['Throwable|ParseError|null'], -'ParseError::getTrace' => ['array'], +'ParseError::getTrace' => ['list\',args?:list,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'], @@ -8432,55 +7781,55 @@ '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::errorCode' => ['string|null'], 'PDO::errorInfo' => ['array'], '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'], 'PDO::query\'3' => ['PDOStatement|false', 'sql'=>'string', 'fetch_into'=>'int', 'object'=>'object'], -'PDO::quote' => ['string', 'string'=>'string', 'paramtype='=>'int'], +'PDO::quote' => ['__benevolent', 'string'=>'string', 'paramtype='=>'int'], 'PDO::rollBack' => ['bool'], '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?:list,object?:object}>'], 'PDOException::getTraceAsString' => [''], -'PDOStatement::__sleep' => ['array'], +'PDOStatement::__sleep' => ['list'], 'PDOStatement::__wakeup' => ['void'], 'PDOStatement::bindColumn' => ['bool', 'column'=>'mixed', '&w_param'=>'mixed', 'type='=>'int', 'maxlen='=>'int', 'driverdata='=>'mixed'], 'PDOStatement::bindParam' => ['bool', 'parameter'=>'mixed', '&w_variable'=>'mixed', 'data_type='=>'int', 'length='=>'int', 'driver_options='=>'mixed'], 'PDOStatement::bindValue' => ['bool', 'parameter'=>'mixed', 'value'=>'mixed', 'data_type='=>'int'], 'PDOStatement::closeCursor' => ['bool'], -'PDOStatement::columnCount' => ['int'], +'PDOStatement::columnCount' => ['0|positive-int'], 'PDOStatement::debugDumpParams' => ['void'], -'PDOStatement::errorCode' => ['string'], +'PDOStatement::errorCode' => ['string|null'], 'PDOStatement::errorInfo' => ['array'], 'PDOStatement::execute' => ['bool', 'bound_input_params='=>'?array'], 'PDOStatement::fetch' => ['mixed', 'how='=>'int', 'orientation='=>'int', 'offset='=>'int'], @@ -8490,11 +7839,11 @@ 'PDOStatement::getAttribute' => ['mixed', 'attribute'=>'int'], 'PDOStatement::getColumnMeta' => ['array|false', 'column'=>'int'], 'PDOStatement::nextRowset' => ['bool'], -'PDOStatement::rowCount' => ['int'], +'PDOStatement::rowCount' => ['0|positive-int'], 'PDOStatement::setAttribute' => ['bool', 'attribute'=>'int', 'value'=>'mixed'], 'PDOStatement::setFetchMode' => ['bool', 'mode'=>'int'], 'PDOStatement::setFetchMode\'1' => ['bool', 'fetch_column'=>'int', 'colno'=>'int'], -'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs'=>'array'], +'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs='=>'?array'], 'PDOStatement::setFetchMode\'3' => ['bool', 'fetch_into'=>'int', 'object'=>'object'], 'pfsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'pg_affected_rows' => ['int', 'result'=>'resource'], @@ -8523,15 +7872,15 @@ 'pg_escape_string\'1' => ['string', 'data'=>'string'], 'pg_execute' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'params'=>'array'], 'pg_execute\'1' => ['resource|false', 'stmtname'=>'string', 'params'=>'array'], -'pg_fetch_all' => ['array|false', 'result'=>'resource', 'result_type='=>'int'], +'pg_fetch_all' => ['array>', 'result'=>'resource', 'result_type='=>'int'], 'pg_fetch_all_columns' => ['array|false', 'result'=>'resource', 'column_number='=>'int'], 'pg_fetch_array' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], -'pg_fetch_assoc' => ['array|false', 'result'=>'resource', 'row='=>'?int'], +'pg_fetch_assoc' => ['non-empty-array|false', 'result'=>'resource', 'row='=>'?int'], 'pg_fetch_object' => ['object|false', 'result'=>'', 'row='=>'?int', 'result_type='=>'int'], 'pg_fetch_object\'1' => ['object', 'result'=>'', 'row='=>'?int', 'class_name='=>'string', 'ctor_params='=>'array'], 'pg_fetch_result' => ['', 'result'=>'', 'field_name'=>'string|int'], 'pg_fetch_result\'1' => ['', 'result'=>'', 'row_number'=>'int', 'field_name'=>'string|int'], -'pg_fetch_row' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], +'pg_fetch_row' => ['non-empty-list|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], 'pg_field_is_null' => ['int|false', 'result'=>'', 'field_name_or_number'=>'string|int'], 'pg_field_is_null\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], 'pg_field_name' => ['string|false', 'result'=>'resource', 'field_number'=>'int'], @@ -8556,8 +7905,8 @@ '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|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], -'pg_lo_import\'1' => ['int', 'pathname'=>'string', 'oid'=>''], +'pg_lo_import' => ['int|string|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], +'pg_lo_import\'1' => ['int|string', 'pathname'=>'string', 'oid'=>''], 'pg_lo_open' => ['resource|false', 'connection'=>'resource', 'oid'=>'int', 'mode'=>'string'], 'pg_lo_read' => ['string|false', 'large_object'=>'resource', 'len='=>'int'], 'pg_lo_read_all' => ['int', 'large_object'=>'resource'], @@ -8572,7 +7921,7 @@ 'pg_options' => ['string', 'connection='=>'resource'], 'pg_parameter_status' => ['string|false', 'connection'=>'resource', 'param_name'=>'string'], 'pg_parameter_status\'1' => ['string|false', 'param_name'=>'string'], -'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'host='=>'string', 'port='=>'string|int', 'options='=>'string', 'tty='=>'string', 'database='=>'string'], +'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'connect_type='=>'int'], 'pg_ping' => ['bool', 'connection='=>'resource'], 'pg_port' => ['int', 'connection='=>'resource'], 'pg_prepare' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'query'=>'string'], @@ -8612,7 +7961,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'], @@ -8669,7 +8018,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'], @@ -8721,10 +8070,10 @@ 'phdfs::tell' => ['int', 'path'=>'string'], 'phdfs::write' => ['bool', 'path'=>'string', 'buffer'=>'string', 'mode='=>'string'], 'php_check_syntax' => ['bool', 'filename'=>'string', 'error_message='=>'string'], -'php_ini_loaded_file' => ['string|false'], +'php_ini_loaded_file' => ['non-empty-string|false'], 'php_ini_scanned_files' => ['string|false'], 'php_logo_guid' => ['string'], -'php_sapi_name' => ['string|false'], +'php_sapi_name' => ['__benevolent'], 'php_strip_whitespace' => ['string', 'file_name'=>'string'], 'php_uname' => ['string', 'mode='=>'string'], 'php_user_filter::filter' => ['int', 'in'=>'resource', 'out'=>'resource', '&rw_consumed'=>'int', 'closing'=>'bool'], @@ -8741,7 +8090,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'], @@ -8798,16 +8148,16 @@ 'posix_getegid' => ['int'], 'posix_geteuid' => ['int'], 'posix_getgid' => ['int'], -'posix_getgrgid' => ['array|false', 'gid'=>'int'], -'posix_getgrnam' => ['array|false', 'groupname'=>'string'], -'posix_getgroups' => ['array|false'], +'posix_getgrgid' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'gid'=>'int'], +'posix_getgrnam' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'groupname'=>'string'], +'posix_getgroups' => ['list|false'], 'posix_getlogin' => ['string|false'], 'posix_getpgid' => ['int|false', 'pid'=>'int'], 'posix_getpgrp' => ['int'], 'posix_getpid' => ['int'], 'posix_getppid' => ['int'], -'posix_getpwnam' => ['array|false', 'groupname'=>'string'], -'posix_getpwuid' => ['array|false', 'uid'=>'int'], +'posix_getpwnam' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'groupname'=>'string'], +'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'], @@ -8830,20 +8180,19 @@ 'Postal\Expand::expand_address' => ['string[]', 'address'=>'string', 'options='=>'array'], 'Postal\Parser::parse_address' => ['array', 'address'=>'string', 'options='=>'array'], 'pow' => ['float|int', 'base'=>'int|float', 'exponent'=>'int|float'], -'preg_filter' => ['mixed', 'regex'=>'mixed', 'replace'=>'mixed', 'subject'=>'mixed', 'limit='=>'int', '&w_count='=>'int'], +'preg_filter' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_grep' => ['array|false', 'regex'=>'string', 'input'=>'array', 'flags='=>'int'], 'preg_last_error' => ['int'], -'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], -'preg_match_all' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], +'preg_match' => ['0|1|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], +'preg_match_all' => ['0|positive-int|false|null', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_quote' => ['string', 'str'=>'string', 'delim_char='=>'string'], 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], +'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['array|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], +'preg_split' => ['list|list}>|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], 'prev' => ['mixed', '&rw_array_arg'=>'array|object'], -'print' => ['int', 'arg'=>'string'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], -'printf' => ['int', 'format'=>'string', '...values='=>'string|int|float'], +'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'proc_close' => ['int', 'process'=>'resource'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'proc_nice' => ['bool', 'priority'=>'int'], @@ -9061,7 +8410,7 @@ 'radius_strerror' => ['string', 'radius_handle'=>'resource'], 'rand' => ['int', 'min'=>'int', 'max'=>'int'], 'rand\'1' => ['int'], -'random_bytes' => ['string', 'length'=>'int'], +'random_bytes' => ['non-empty-string', 'length'=>'positive-int'], 'random_int' => ['int', 'min'=>'int', 'max'=>'int'], 'range' => ['array', 'low'=>'int|float|string', 'high'=>'int|float|string', 'step='=>'int|float'], 'RangeException::__clone' => ['void'], @@ -9072,7 +8421,7 @@ 'RangeException::getLine' => ['int'], 'RangeException::getMessage' => ['string'], 'RangeException::getPrevious' => ['Throwable|RangeException|null'], -'RangeException::getTrace' => ['array'], +'RangeException::getTrace' => ['list\',args?:list,object?:object}>'], 'RangeException::getTraceAsString' => ['string'], 'rar_allow_broken_set' => ['bool', 'rarfile'=>'RarArchive', 'allow_broken'=>'bool'], 'rar_broken_is' => ['bool', 'rarfile'=>'RarArchive'], @@ -9115,16 +8464,16 @@ 'RarException::getLine' => ['int'], 'RarException::getMessage' => ['string'], 'RarException::getPrevious' => ['Exception|Throwable'], -'RarException::getTrace' => ['array'], +'RarException::getTrace' => ['list\',args?:list,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'], @@ -9139,7 +8488,7 @@ 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'string'], 'readlink' => ['string|false', 'filename'=>'string'], -'realpath' => ['string|false', 'path'=>'string'], +'realpath' => ['non-empty-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'str'=>'string'], @@ -9181,7 +8530,7 @@ 'RecursiveCachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags'=>''], 'RecursiveCachingIterator::getChildren' => ['RecursiveCachingIterator'], 'RecursiveCachingIterator::hasChildren' => ['bool'], -'RecursiveCallbackFilterIterator::__construct' => ['void', 'iterator'=>'recursiveiterator', 'func'=>'callable'], +'RecursiveCallbackFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'func'=>'callable'], 'RecursiveCallbackFilterIterator::getChildren' => ['RecursiveCallbackFilterIterator'], 'RecursiveCallbackFilterIterator::hasChildren' => ['void'], 'RecursiveDirectoryIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], @@ -9192,12 +8541,12 @@ 'RecursiveDirectoryIterator::key' => ['string'], 'RecursiveDirectoryIterator::next' => ['void'], 'RecursiveDirectoryIterator::rewind' => ['void'], -'RecursiveFilterIterator::__construct' => ['void', 'iterator'=>'recursiveiterator'], +'RecursiveFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'RecursiveFilterIterator::getChildren' => ['RecursiveFilterIterator'], 'RecursiveFilterIterator::hasChildren' => ['bool'], 'RecursiveIterator::getChildren' => ['RecursiveIterator'], 'RecursiveIterator::hasChildren' => ['bool'], -'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'int', 'flags='=>'int'], +'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], 'RecursiveIteratorIterator::beginChildren' => ['void'], 'RecursiveIteratorIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveIteratorIterator::callGetChildren' => ['RecursiveIterator'], @@ -9215,10 +8564,10 @@ 'RecursiveIteratorIterator::rewind' => ['void'], 'RecursiveIteratorIterator::setMaxDepth' => ['void', 'max_depth='=>'int'], 'RecursiveIteratorIterator::valid' => ['bool'], -'RecursiveRegexIterator::__construct' => ['void', 'iterator'=>'recursiveiterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RecursiveRegexIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RecursiveRegexIterator::getChildren' => ['RecursiveRegexIterator'], 'RecursiveRegexIterator::hasChildren' => ['bool'], -'RecursiveTreeIterator::__construct' => ['void', 'iterator'=>'recursiveiterator|iteratoraggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], +'RecursiveTreeIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], 'RecursiveTreeIterator::beginChildren' => ['void'], 'RecursiveTreeIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveTreeIterator::callGetChildren' => ['RecursiveIterator'], @@ -9236,233 +8585,243 @@ 'RecursiveTreeIterator::setPostfix' => ['void', 'prefix'=>'string'], 'RecursiveTreeIterator::setPrefixPart' => ['void', 'part'=>'int', 'prefix'=>'string'], 'RecursiveTreeIterator::valid' => ['bool'], -'Redis::__construct' => ['void'], -'Redis::_prefix' => ['string', 'value'=>'mixed'], -'Redis::_serialize' => ['mixed', 'value'=>'mixed'], -'Redis::_unserialize' => ['mixed', 'value'=>'string'], -'Redis::append' => ['int', 'key'=>'string', 'value'=>'string'], -'Redis::auth' => ['bool', 'password'=>'string|string[]'], -'Redis::bgRewriteAOF' => ['bool'], -'Redis::bgSave' => ['bool'], -'Redis::bitCount' => ['int', 'key'=>'string'], -'Redis::bitOp' => ['int', 'operation'=>'string', '...args'=>'string'], -'Redis::bitpos' => ['int', 'key'=>'string', 'bit'=>'int', 'start='=>'int', 'end='=>'int'], -'Redis::blPop' => ['array', 'keys'=>'string[]', 'timeout'=>'int'], +'Redis::__construct' => ['void', 'options='=>'?array{host?:string,port?:int,connectTimeout?:float,auth?:list{string|null|false,string}|list{string},ssl?:array,backoff?:array}'], +'Redis::_serialize' => ['mixed', 'value'=>'string'], +'Redis::_unserialize' => ['string', 'value'=>'mixed'], +'Redis::_pack' => ['mixed', 'value'=>'string'], +'Redis::_unpack' => ['string', 'value'=>'mixed'], +'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::clearLastError' => ['bool'], -'Redis::client' => ['mixed', 'command'=>'string', 'arg='=>'string'], -'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::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::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::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::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::getDel' => ['__benevolent', 'key'=>'string'], 'Redis::getKeys' => ['array', 'pattern'=>'string'], -'Redis::getLastError' => ['null|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::lastSave' => ['int'], +'Redis::getRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::getset' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'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::keys' => ['__benevolent|false>', 'pattern'=>'string'], +'Redis::lcs' => ['__benevolent', 'key1'=>'string', 'key2'=>'string', 'options'=>'?array{MINMATCHLEN?:int,WITHMATCHLEN?:bool,LEN?:bool,IDX?:bool}'], 'Redis::lGet' => ['', 'key'=>'string', 'index'=>'int'], 'Redis::lGetRange' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lIndex' => ['string|false', 'key'=>'string', 'index'=>'int'], -'Redis::lInsert' => ['int', 'key'=>'string', 'position'=>'int', 'pivot'=>'string', 'value'=>'string'], +'Redis::lindex' => ['null|string|false', 'key'=>'string', 'index'=>'int'], +'Redis::lInsert' => ['__benevolent', 'key'=>'string', 'pos'=>'int', 'pivot'=>'mixed', 'value'=>'mixed'], 'Redis::listTrim' => ['', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::lLen' => ['int|false', 'key'=>'string'], -'Redis::lPop' => ['string', 'key'=>'string'], -'Redis::lPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::lPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::lRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lRem' => ['int|false', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lRemove' => ['', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lSet' => ['bool', 'key'=>'string', 'index'=>'int', 'value'=>'string'], +'Redis::lLen' => ['__benevolent', 'key'=>'string'], +'Redis::lMove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'wherefrom'=>'string', 'whereto'=>'string'], +'Redis::lmpop' => ['__benevolent|null|false>', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::lPop' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::lPos' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options'=>'?array{COUNT?:int,RANK?:int,MAXLEN?:int}'], +'Redis::lPush' => ['__benevolent', 'key'=>'string', '...elements='=>'mixed'], +'Redis::lPushx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::lrange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::lrem' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'count='=>'int'], +'Redis::lSet' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'mixed'], 'Redis::lSize' => ['', 'key'=>'string'], -'Redis::lTrim' => ['array|false', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::mGet' => ['array', 'keys'=>'string[]'], -'Redis::migrate' => ['bool', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'db'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool'], -'Redis::move' => ['bool', 'key'=>'string', 'dbindex'=>'int'], -'Redis::mSet' => ['bool', 'pairs'=>'array'], -'Redis::mSetNx' => ['bool', 'pairs'=>'array'], -'Redis::multi' => ['Redis', 'mode='=>'int'], -'Redis::object' => ['string|long|false', 'info'=>'string', 'key'=>'string'], -'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'reserved='=>'null', 'retry_interval='=>'?int', 'read_timeout='=>'float'], -'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::persist' => ['bool', 'key'=>'string'], -'Redis::pExpire' => ['bool', 'key'=>'string', 'ttl'=>'int'], -'Redis::pexpireAt' => ['bool', 'key'=>'string', 'expiry'=>'int'], -'Redis::pfAdd' => ['bool', 'key'=>'string', 'elements'=>'array'], -'Redis::pfCount' => ['int', 'key'=>'array|string'], -'Redis::pfMerge' => ['bool', 'destkey'=>'string', 'sourcekeys'=>'array'], -'Redis::ping' => ['string'], -'Redis::popen' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::psetex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::psubscribe' => ['', 'patterns'=>'array', 'callback'=>'array|string'], -'Redis::pttl' => ['int|false', 'key'=>'string'], -'Redis::publish' => ['int', 'channel'=>'string', 'message'=>'string'], -'Redis::pubsub' => ['array|int', 'keyword'=>'string', 'argument'=>'array|string'], -'Redis::punsubscribe' => ['', 'pattern'=>'string', '...other_patterns='=>'string'], -'Redis::randomKey' => ['string'], -'Redis::rawCommand' => ['mixed', 'command'=>'string', '...arguments='=>'mixed'], -'Redis::rename' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::ltrim' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::mget' => ['__benevolent>', 'keys'=>'string[]'], +'Redis::migrate' => ['__benevolent', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'dstdb'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool', 'credentials='=>'mixed'], +'Redis::move' => ['__benevolent', 'key'=>'string', 'index'=>'int'], +'Redis::mset' => ['__benevolent', 'key_values'=>'array'], +'Redis::msetnx' => ['__benevolent', 'key_values'=>'array'], +'Redis::multi' => ['__benevolent', 'value='=>'int'], +'Redis::object' => ['__benevolent', 'subcommand'=>'string', 'key'=>'string'], +'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::persist' => ['__benevolent', 'key'=>'string'], +'Redis::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::rename' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], 'Redis::renameKey' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], -'Redis::renameNx' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::renameNx' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], +'Redis::reset' => ['__benevolent'], 'Redis::resetStat' => ['bool'], -'Redis::restore' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::rPop' => ['string', 'key'=>'string'], -'Redis::rpoplpush' => ['string', 'srcKey'=>'string', 'dstKey'=>'string'], -'Redis::rPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::rPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::sAdd' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::sAddArray' => ['bool', 'key'=>'string', 'values'=>'array'], -'Redis::save' => ['bool'], -'Redis::scan' => ['array|false', '&w_iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int'], -'Redis::sCard' => ['int', 'key'=>'string'], +'Redis::restore' => ['__benevolent', 'key'=>'string', 'ttl'=>'int', 'value'=>'string', 'options='=>'?array{ABSTTL?:bool,REPLACE?:bool,IDLETIME?:int,FREQ?:int}'], +'Redis::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::save' => ['__benevolent'], +'Redis::scan' => ['array|false', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int', 'type='=>'?string'], +'Redis::scard' => ['__benevolent', 'key'=>'string'], 'Redis::sContains' => ['', 'key'=>'string', 'value'=>'string'], -'Redis::script' => ['mixed', 'command'=>'string', '...args='=>'mixed'], -'Redis::sDiff' => ['array', 'key1'=>'string', '...other_keys='=>'string'], -'Redis::sDiffStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::select' => ['bool', 'dbindex'=>'int'], -'Redis::set' => ['bool', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], +'Redis::sDiff' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sDiffStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::select' => ['__benevolent', 'db'=>'int'], +'Redis::set' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], 'Redis::set\'1' => ['bool', 'key'=>'string', 'value'=>'mixed', 'timeout='=>'int'], -'Redis::setBit' => ['int', 'key'=>'string', 'offset'=>'int', 'value'=>'int'], -'Redis::setEx' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setNx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setnx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setOption' => ['bool', 'name'=>'int', 'value'=>'mixed'], -'Redis::setRange' => ['int', 'key'=>'string', 'offset'=>'int', 'end'=>'string'], +'Redis::setBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int', 'value'=>'bool'], +'Redis::setex' => ['__benevolent', 'key'=>'string', 'expire'=>'int', 'value'=>'mixed'], +'Redis::setnx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::setRange' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'string'], 'Redis::setTimeout' => ['bool', 'key'=>'string', 'ttl'=>'int'], 'Redis::sGetMembers' => ['array', 'key'=>'string'], -'Redis::sInter' => ['array|false', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sInterStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sIsMember' => ['bool', 'key'=>'string', 'value'=>'string'], +'Redis::sInter' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sintercard' => ['__benevolent', 'keys'=>'string[]', 'limit='=>'int'], +'Redis::sInterStore' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'Redis::sismember' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], 'Redis::slave' => ['bool', 'host'=>'string', 'port'=>'int'], 'Redis::slave\'1' => ['bool', 'host'=>'string', 'port'=>'int'], -'Redis::slaveof' => ['bool', 'host='=>'string', 'port='=>'int'], -'Redis::slowLog' => ['mixed', 'operation'=>'string', 'length='=>'int'], -'Redis::sMembers' => ['array', 'key'=>'string'], -'Redis::sMove' => ['bool', 'srcKey'=>'string', 'dstKey'=>'string', 'member'=>'string'], -'Redis::sort' => ['array|int', 'key'=>'string', 'options='=>'array'], -'Redis::sPop' => ['string|false', 'key'=>'string'], -'Redis::sRandMember' => ['array|string|false', 'key'=>'string', 'count='=>'int'], -'Redis::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], +'Redis::slaveof' => ['__benevolent', 'host='=>'?string', 'port='=>'int'], +'Redis::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' => ['mixed|null', 'channels'=>'array', 'callback'=>'string|array'], +'Redis::sscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::ssubscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], +'Redis::strlen' => ['__benevolent', 'key'=>'string'], +'Redis::subscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], 'Redis::substr' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::sUnion' => ['array', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sUnionStore' => ['int', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::time' => ['array'], -'Redis::ttl' => ['int|false', 'key'=>'string'], -'Redis::type' => ['int', 'key'=>'string'], -'Redis::unlink' => ['int', 'key'=>'string', '...args'=>'string'], 'Redis::unlink\'1' => ['int', 'key'=>'string[]'], -'Redis::unsubscribe' => ['', 'channel'=>'string', '...other_channels='=>'string'], -'Redis::unwatch' => [''], -'Redis::wait' => ['int', 'numSlaves'=>'int', 'timeout'=>'int'], -'Redis::watch' => ['void', 'key'=>'string', '...other_keys='=>'string'], -'Redis::xack' => ['', 'str_key'=>'string', 'str_group'=>'string', 'arr_ids'=>'array'], -'Redis::xadd' => ['', 'str_key'=>'string', 'str_id'=>'string', 'arr_fields'=>'array', 'i_maxlen='=>'', 'boo_approximate='=>''], -'Redis::xclaim' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_consumer'=>'string', 'i_min_idle'=>'', 'arr_ids'=>'array', 'arr_opts='=>'array'], -'Redis::xdel' => ['', 'str_key'=>'string', 'arr_ids'=>'array'], -'Redis::xgroup' => ['', 'str_operation'=>'string', 'str_key='=>'string', 'str_arg1='=>'', 'str_arg2='=>'', 'str_arg3='=>''], -'Redis::xinfo' => ['', 'str_cmd'=>'string', 'str_key='=>'string', 'str_group='=>'string'], -'Redis::xlen' => ['', 'key'=>''], -'Redis::xpending' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_start='=>'', 'str_end='=>'', 'i_count='=>'', 'str_consumer='=>'string'], -'Redis::xrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xread' => ['', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xreadgroup' => ['', 'str_group'=>'string', 'str_consumer'=>'string', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xrevrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xtrim' => ['', 'str_key'=>'string', 'i_maxlen'=>'', 'boo_approximate='=>''], -'Redis::zAdd' => ['int', 'key'=>'string', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], -'Redis::zAdd\'1' => ['int', 'options'=>'array', 'key'=>'string', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], -'Redis::zCard' => ['int', 'key'=>'string'], -'Redis::zCount' => ['int', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::sUnion' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sUnionStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sunsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::swapdb' => ['__benevolent', 'src'=>'int', 'dst'=>'int'], +'Redis::time' => ['__benevolent'], +'Redis::ttl' => ['__benevolent', 'key'=>'string'], +'Redis::type' => ['__benevolent', 'key'=>'string'], +'Redis::unlink' => ['__benevolent', 'key'=>'string[]|string', '...other_keys'=>'string'], +'Redis::unsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::unwatch' => ['__benevolent'], +'Redis::watch' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'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::xlen' => ['__benevolent', 'key'=>'string'], +'Redis::xpending' => ['__benevolent', 'key'=>'string', 'group'=>'string', 'start='=>'?string', 'end='=>'?string', 'count='=>'int', 'consumer='=>'?string'], +'Redis::xrange' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'count='=>'int'], +'Redis::xread' => ['__benevolent', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xreadgroup' => ['__benevolent', 'group'=>'string', 'consumer'=>'string', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xrevrange' => ['__benevolent', 'key'=>'string', 'end'=>'string', 'start'=>'string', 'count='=>'int'], +'Redis::xtrim' => ['__benevolent', 'key'=>'string', 'threshold'=>'string', 'approx='=>'bool', 'minid='=>'bool', 'limit='=>'int'], +'Redis::zAdd' => ['__benevolent', 'key'=>'string', 'score_or_options'=>'array|float', '...more_scores_and_mems='=>'mixed'], +'Redis::zAdd\'1' => ['int', 'key'=>'string', 'options'=>'array', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], +'Redis::zCard' => ['__benevolent', 'key'=>'string'], +'Redis::zCount' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], 'Redis::zDelete' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zDeleteRangeByRank' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], 'Redis::zDeleteRangeByScore' => ['', 'key'=>'string', 'start'=>'float', 'end'=>'float'], -'Redis::zIncrBy' => ['float', 'key'=>'string', 'value'=>'float', 'member'=>'string'], -'Redis::zInter' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], -'Redis::zRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscores='=>'bool'], -'Redis::zRangeByLex' => ['array|false', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRangeByScore' => ['array', 'key'=>'string', 'start'=>'int|string', 'end'=>'int|string', 'options='=>'array'], -'Redis::zRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zRem' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::zIncrBy' => ['__benevolent', 'key'=>'string', 'value'=>'float', 'member'=>'mixed'], +'Redis::zInter' => ['__benevolent', 'keys'=>'string[]', 'weights='=>'?array', 'options='=>'?array'], +'Redis::zmpop' => ['__benevolent', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::zRange' => ['__benevolent', 'key'=>'string', 'start'=>'string|int', 'end'=>'string|int', 'options='=>'array|bool|null'], +'Redis::zRangeByLex' => ['__benevolent', 'key'=>'string', 'min'=>'string', 'max'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'options='=>'array'], +'Redis::zRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zRem' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zRemove' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::zRemRangeByRank' => ['int', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::zRemRangeByScore' => ['int', 'key'=>'string', 'start'=>'float|string', 'end'=>'float|string'], -'Redis::zRevRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscore='=>'bool'], -'Redis::zRevRangeByLex' => ['array', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRevRangeByScore' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'options='=>'array'], -'Redis::zRevRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zScan' => ['array|bool', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::zScore' => ['float|false', 'key'=>'string', 'member'=>'string'], +'Redis::zRemRangeByRank' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::zRemRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::zRevRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'scores='=>'bool|array{withscores:bool}|null'], +'Redis::zRevRangeByLex' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRevRangeByScore' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'options='=>'array|bool'], +'Redis::zRevRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::zScore' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], 'Redis::zSize' => ['', 'key'=>'string'], -'Redis::zUnion' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], +'Redis::zUnion' => ['__benevolent', 'keys'=>'string[]', 'weights'=>'?array', 'options='=>'?array'], 'RedisArray::__construct' => ['void', 'name'=>'string'], 'RedisArray::__construct\'1' => ['void', 'hosts'=>'array', 'opts='=>'array'], 'RedisArray::_function' => ['string'], 'RedisArray::_hosts' => ['array'], 'RedisArray::_rehash' => ['', 'callable='=>'callable'], 'RedisArray::_target' => ['string', 'key'=>'string'], -'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds'=>'array', 'timeout='=>'float', 'readTimeout='=>'float', 'persistent='=>'bool|false', 'auth='=>'string|array|null'], -'RedisCluster::_masters' => ['array'], +'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds='=>'string[]|null', 'timeout='=>'int|float', 'read_timeout='=>'int|float', 'persistent='=>'bool', 'auth='=>'mixed', 'context='=>'array|null'], 'RedisCluster::_prefix' => ['string', 'value'=>'mixed'], 'RedisCluster::_serialize' => ['mixed', 'value'=>'mixed'], -'RedisCluster::_unserialize' => ['mixed', 'value'=>'string'], 'RedisCluster::append' => ['int', 'key'=>'string', 'value'=>'string'], 'RedisCluster::bgrewriteaof' => ['bool', 'nodeParams'=>'string'], 'RedisCluster::bgsave' => ['bool', 'nodeParams'=>'string'], @@ -9472,7 +8831,6 @@ 'RedisCluster::blPop' => ['array', 'keys'=>'array', 'timeout'=>'int'], 'RedisCluster::brPop' => ['array', 'keys'=>'array', 'timeout'=>'int'], 'RedisCluster::brpoplpush' => ['string', 'srcKey'=>'string', 'dstKey'=>'string', 'timeout'=>'int'], -'RedisCluster::clearLastError' => ['bool'], 'RedisCluster::client' => ['', 'nodeParams'=>'string', 'subCmd'=>'', 'args'=>''], 'RedisCluster::close' => [''], 'RedisCluster::cluster' => ['mixed', 'nodeParams'=>'string', 'command'=>'string', 'arguments'=>'mixed'], @@ -9481,12 +8839,11 @@ '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'], 'RedisCluster::eval' => ['mixed', 'script'=>'', 'args='=>'', 'numKeys='=>''], -'RedisCluster::evalSha' => ['mixed', 'scriptSha'=>'string', 'args='=>'array', 'numKeys='=>'int'], 'RedisCluster::exec' => ['array|void'], 'RedisCluster::exists' => ['bool', 'key'=>'string'], 'RedisCluster::expire' => ['bool', 'key'=>'string', 'ttl'=>'int'], @@ -9502,8 +8859,7 @@ 'RedisCluster::get' => ['bool|string', 'key'=>'string'], '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'], @@ -9540,7 +8896,7 @@ 'RedisCluster::mget' => ['array', 'array'=>'array'], 'RedisCluster::mset' => ['bool', 'array'=>'array'], 'RedisCluster::msetnx' => ['int', 'array'=>'array'], -'RedisCluster::multi' => ['Redis', 'mode='=>'int'], +'RedisCluster::multi' => ['__benevolent', 'mode='=>'int'], 'RedisCluster::object' => ['string', 'string='=>'string', 'key='=>'string'], 'RedisCluster::persist' => ['bool', 'key'=>'string'], 'RedisCluster::pExpire' => ['bool', 'key'=>'string', 'ttl'=>'int'], @@ -9568,29 +8924,28 @@ 'RedisCluster::sAdd' => ['int', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], 'RedisCluster::sAddArray' => ['int', 'key'=>'string', 'valueArray'=>'array'], 'RedisCluster::save' => ['bool', 'nodeParams'=>'string'], -'RedisCluster::scan' => ['array', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], +'RedisCluster::scan' => ['array|bool', '&iterator'=>'int|null', 'key_or_address' => 'string|array', '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::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'], @@ -9635,232 +8990,48 @@ 'RedisCluster::zScan' => ['array', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], 'RedisCluster::zScore' => ['float', 'key'=>'string', 'member'=>'string'], 'RedisCluster::zUnionStore' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], -'Reflection::export' => ['string|null', 'r'=>'reflector', 'return='=>'bool'], -'Reflection::getModifierNames' => ['array', 'modifiers'=>'int'], -'ReflectionClass::__clone' => ['void'], -'ReflectionClass::__construct' => ['void', 'argument'=>'object|string'], -'ReflectionClass::__toString' => ['string'], 'ReflectionClass::export' => ['string|null', 'argument'=>'string|object', 'return='=>'bool'], -'ReflectionClass::getConstant' => ['mixed', 'name'=>'string'], 'ReflectionClass::getConstants' => ['array'], 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], 'ReflectionClass::getDefaultProperties' => ['array'], -'ReflectionClass::getDocComment' => ['string|false'], -'ReflectionClass::getEndLine' => ['int|false'], -'ReflectionClass::getExtension' => ['ReflectionExtension|null'], -'ReflectionClass::getExtensionName' => ['string|false'], -'ReflectionClass::getFileName' => ['string|false'], -'ReflectionClass::getInterfaceNames' => ['array'], +'ReflectionClass::getEndLine' => ['positive-int|false'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], +'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], -'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], -'ReflectionClass::getMethods' => ['array', 'filter='=>'int'], -'ReflectionClass::getModifiers' => ['int'], +'ReflectionClass::getMethods' => ['list', 'filter='=>'int'], 'ReflectionClass::getName' => ['class-string'], -'ReflectionClass::getNamespaceName' => ['string'], -'ReflectionClass::getParentClass' => ['ReflectionClass|false'], -'ReflectionClass::getProperties' => ['array', 'filter='=>'int'], -'ReflectionClass::getProperty' => ['ReflectionProperty', 'name'=>'string'], -'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], -'ReflectionClass::getReflectionConstants' => ['array'], -'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], +'ReflectionClass::getProperties' => ['list', 'filter='=>'int'], +'ReflectionClass::getReflectionConstants' => ['list'], +'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::inNamespace' => ['bool'], -'ReflectionClass::isAbstract' => ['bool'], -'ReflectionClass::isAnonymous' => ['bool'], -'ReflectionClass::isCloneable' => ['bool'], -'ReflectionClass::isFinal' => ['bool'], -'ReflectionClass::isInstance' => ['bool', 'object'=>'object'], -'ReflectionClass::isInstantiable' => ['bool'], -'ReflectionClass::isInterface' => ['bool'], -'ReflectionClass::isInternal' => ['bool'], -'ReflectionClass::isIterable' => ['bool'], -'ReflectionClass::isIterateable' => ['bool'], -'ReflectionClass::isSubclassOf' => ['bool', 'class'=>'string|reflectionclass'], -'ReflectionClass::isTrait' => ['bool'], -'ReflectionClass::isUserDefined' => ['bool'], +'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'string|ReflectionClass'], 'ReflectionClass::newInstance' => ['object', 'args='=>'mixed', '...args='=>'mixed'], 'ReflectionClass::newInstanceArgs' => ['object', 'args='=>'array'], -'ReflectionClass::newInstanceWithoutConstructor' => ['object'], -'ReflectionClass::setStaticPropertyValue' => ['void', 'name'=>'string', 'value'=>'string'], -'ReflectionClassConstant::__construct' => ['void', 'class'=>'mixed', 'name'=>'string'], -'ReflectionClassConstant::__toString' => ['string'], -'ReflectionClassConstant::export' => ['string', 'class'=>'mixed', 'name'=>'string', 'return='=>'bool'], -'ReflectionClassConstant::getDeclaringClass' => ['ReflectionClass'], -'ReflectionClassConstant::getDocComment' => ['string|false'], -'ReflectionClassConstant::getModifiers' => ['int'], 'ReflectionClassConstant::getName' => ['string'], -'ReflectionClassConstant::getValue' => ['mixed'], -'ReflectionClassConstant::isPrivate' => ['bool'], -'ReflectionClassConstant::isProtected' => ['bool'], -'ReflectionClassConstant::isPublic' => ['bool'], -'ReflectionExtension::__clone' => ['void'], -'ReflectionExtension::__construct' => ['void', 'name'=>'string'], -'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'], 'ReflectionExtension::getINIEntries' => ['array'], -'ReflectionExtension::getName' => ['string'], -'ReflectionExtension::getVersion' => ['string'], -'ReflectionExtension::info' => ['void'], -'ReflectionExtension::isPersistent' => ['void'], -'ReflectionExtension::isTemporary' => ['bool'], -'ReflectionFunction::__construct' => ['void', 'name'=>'string|Closure'], -'ReflectionFunction::__toString' => ['string'], -'ReflectionFunction::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], -'ReflectionFunction::getClosure' => ['?Closure'], -'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], -'ReflectionFunction::getClosureThis' => ['bool'], -'ReflectionFunction::getDocComment' => ['string|false'], -'ReflectionFunction::getEndLine' => ['int|false'], -'ReflectionFunction::getExtension' => ['ReflectionExtension|null'], -'ReflectionFunction::getExtensionName' => ['string|false'], -'ReflectionFunction::getFileName' => ['string|false'], -'ReflectionFunction::getName' => ['string'], -'ReflectionFunction::getNamespaceName' => ['string'], -'ReflectionFunction::getNumberOfParameters' => ['int'], -'ReflectionFunction::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunction::getParameters' => ['array'], -'ReflectionFunction::getReturnType' => ['?ReflectionType'], -'ReflectionFunction::getShortName' => ['string'], -'ReflectionFunction::getStartLine' => ['int|false'], -'ReflectionFunction::getStaticVariables' => ['array'], -'ReflectionFunction::inNamespace' => ['bool'], -'ReflectionFunction::invoke' => ['mixed', '...args='=>'mixed'], -'ReflectionFunction::invokeArgs' => ['mixed', 'args'=>'array'], -'ReflectionFunction::isClosure' => ['bool'], -'ReflectionFunction::isDeprecated' => ['bool'], -'ReflectionFunction::isDisabled' => ['bool'], -'ReflectionFunction::isGenerator' => ['bool'], -'ReflectionFunction::isInternal' => ['bool'], -'ReflectionFunction::isUserDefined' => ['bool'], -'ReflectionFunction::isVariadic' => ['bool'], -'ReflectionFunction::returnsReference' => ['bool'], -'ReflectionFunctionAbstract::__clone' => ['void'], -'ReflectionFunctionAbstract::__toString' => ['string'], -'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::getNamespaceName' => ['string'], -'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], -'ReflectionFunctionAbstract::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunctionAbstract::getParameters' => ['array'], -'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], -'ReflectionFunctionAbstract::getShortName' => ['string'], -'ReflectionFunctionAbstract::getStartLine' => ['int|false'], -'ReflectionFunctionAbstract::getStaticVariables' => ['array'], -'ReflectionFunctionAbstract::hasReturnType' => ['bool'], -'ReflectionFunctionAbstract::inNamespace' => ['bool'], -'ReflectionFunctionAbstract::isClosure' => ['bool'], -'ReflectionFunctionAbstract::isDeprecated' => ['bool'], -'ReflectionFunctionAbstract::isGenerator' => ['bool'], -'ReflectionFunctionAbstract::isInternal' => ['bool'], -'ReflectionFunctionAbstract::isUserDefined' => ['bool'], -'ReflectionFunctionAbstract::isVariadic' => ['bool'], -'ReflectionFunctionAbstract::returnsReference' => ['bool'], -'ReflectionGenerator::__construct' => ['void', 'generator'=>'object'], -'ReflectionGenerator::getExecutingFile' => ['string'], -'ReflectionGenerator::getExecutingGenerator' => ['Generator'], -'ReflectionGenerator::getExecutingLine' => ['int'], -'ReflectionGenerator::getFunction' => ['ReflectionFunctionAbstract'], -'ReflectionGenerator::getThis' => ['object'], -'ReflectionGenerator::getTrace' => ['array', 'options'=>'int'], +'ReflectionFunctionAbstract::getEndLine' => ['positive-int|false'], +'ReflectionFunctionAbstract::getFileName' => ['non-empty-string|false'], +'ReflectionFunctionAbstract::getName' => ['non-empty-string'], +'ReflectionFunctionAbstract::getParameters' => ['list'], +'ReflectionFunctionAbstract::getStartLine' => ['positive-int|false'], +'ReflectionGenerator::getTrace' => ['list\',args?:list,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::getDeclaringClass' => ['ReflectionClass'], -'ReflectionMethod::getModifiers' => ['int'], -'ReflectionMethod::getPrototype' => ['ReflectionMethod'], -'ReflectionMethod::invoke' => ['mixed', 'object'=>'?object', '...args='=>'mixed'], -'ReflectionMethod::invokeArgs' => ['mixed', 'object'=>'?object', 'args'=>'array'], -'ReflectionMethod::isAbstract' => ['bool'], -'ReflectionMethod::isConstructor' => ['bool'], -'ReflectionMethod::isDestructor' => ['bool'], -'ReflectionMethod::isFinal' => ['bool'], -'ReflectionMethod::isPrivate' => ['bool'], -'ReflectionMethod::isProtected' => ['bool'], -'ReflectionMethod::isPublic' => ['bool'], -'ReflectionMethod::isStatic' => ['bool'], -'ReflectionMethod::setAccessible' => ['void', 'visible'=>'bool'], -'ReflectionNamedType::__toString' => ['string'], -'ReflectionNamedType::allowsNull' => ['bool'], -'ReflectionNamedType::getName' => ['string'], -'ReflectionNamedType::isBuiltin' => ['bool'], -'ReflectionObject::__construct' => ['void', 'argument'=>'object'], -'ReflectionObject::export' => ['string|null', 'argument'=>'object', 'return='=>'bool'], -'ReflectionParameter::__clone' => ['void'], -'ReflectionParameter::__construct' => ['void', 'function'=>'', 'parameter'=>''], -'ReflectionParameter::__toString' => ['string'], -'ReflectionParameter::allowsNull' => ['bool'], -'ReflectionParameter::canBePassedByValue' => ['bool'], -'ReflectionParameter::export' => ['string|null', 'function'=>'string', 'parameter'=>'string', 'return='=>'bool'], -'ReflectionParameter::getClass' => ['ReflectionClass|null'], -'ReflectionParameter::getDeclaringClass' => ['ReflectionClass|null'], -'ReflectionParameter::getDeclaringFunction' => ['ReflectionFunctionAbstract'], -'ReflectionParameter::getDefaultValue' => ['mixed'], -'ReflectionParameter::getDefaultValueConstantName' => ['?string'], -'ReflectionParameter::getName' => ['string'], -'ReflectionParameter::getPosition' => ['int'], -'ReflectionParameter::getType' => ['ReflectionType|null'], -'ReflectionParameter::hasType' => ['bool'], -'ReflectionParameter::isArray' => ['bool'], -'ReflectionParameter::isCallable' => ['bool'], -'ReflectionParameter::isDefaultValueAvailable' => ['bool'], -'ReflectionParameter::isDefaultValueConstant' => ['bool'], -'ReflectionParameter::isOptional' => ['bool'], -'ReflectionParameter::isPassedByReference' => ['bool'], -'ReflectionParameter::isVariadic' => ['bool'], -'ReflectionProperty::__clone' => ['void'], -'ReflectionProperty::__construct' => ['void', 'class'=>'', 'name'=>'string'], -'ReflectionProperty::__toString' => ['string'], -'ReflectionProperty::export' => ['string|null', 'class'=>'mixed', 'name'=>'string', 'return='=>'bool'], -'ReflectionProperty::getDeclaringClass' => ['ReflectionClass'], -'ReflectionProperty::getDocComment' => ['string|false'], -'ReflectionProperty::getModifiers' => ['int'], -'ReflectionProperty::getName' => ['string'], -'ReflectionProperty::getValue' => ['mixed', 'object='=>'object'], -'ReflectionProperty::isDefault' => ['bool'], -'ReflectionProperty::isPrivate' => ['bool'], -'ReflectionProperty::isProtected' => ['bool'], -'ReflectionProperty::isPublic' => ['bool'], -'ReflectionProperty::isStatic' => ['bool'], -'ReflectionProperty::setAccessible' => ['void', 'visible'=>'bool'], +'ReflectionParameter::getName' => ['non-empty-string'], +'ReflectionProperty::getName' => ['non-empty-string'], 'ReflectionProperty::setValue' => ['void', 'object'=>'null|object', 'value'=>''], 'ReflectionProperty::setValue\'1' => ['void', 'value'=>''], -'ReflectionType::__toString' => ['string'], -'ReflectionType::allowsNull' => ['bool'], -'ReflectionType::isBuiltin' => ['bool'], -'ReflectionZendExtension::__clone' => ['void'], -'ReflectionZendExtension::__construct' => ['void', 'name'=>'string'], -'ReflectionZendExtension::__toString' => ['string'], -'ReflectionZendExtension::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], -'ReflectionZendExtension::getAuthor' => ['string'], -'ReflectionZendExtension::getCopyright' => ['string'], -'ReflectionZendExtension::getName' => ['string'], -'ReflectionZendExtension::getURL' => ['string'], -'ReflectionZendExtension::getVersion' => ['string'], 'Reflector::__toString' => ['string'], 'Reflector::export' => ['?string'], -'RegexIterator::__construct' => ['void', 'iterator'=>'iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RegexIterator::__construct' => ['void', 'iterator'=>'Iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RegexIterator::accept' => ['bool'], 'RegexIterator::getFlags' => ['int'], 'RegexIterator::getMode' => ['int'], @@ -9881,20 +9052,20 @@ '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|false', 'bundlename'=>'string'], -'restore_error_handler' => ['bool'], -'restore_exception_handler' => ['bool'], +'restore_error_handler' => ['true'], +'restore_exception_handler' => ['true'], 'restore_include_path' => ['void'], 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['__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'], @@ -9962,7 +9133,7 @@ 'RuntimeException::getLine' => ['int'], 'RuntimeException::getMessage' => ['string'], 'RuntimeException::getPrevious' => ['Throwable|RuntimeException|null'], -'RuntimeException::getTrace' => ['array'], +'RuntimeException::getTrace' => ['list\',args?:list,object?:object}>'], 'RuntimeException::getTraceAsString' => ['string'], 'SAMConnection::commit' => ['bool'], 'SAMConnection::connect' => ['bool', 'protocol'=>'string', 'properties='=>'array'], @@ -9995,7 +9166,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'], @@ -10095,11 +9266,11 @@ 'session_destroy' => ['bool'], 'session_encode' => ['string|false'], 'session_gc' => ['int|false'], -'session_get_cookie_params' => ['array'], +'session_get_cookie_params' => ['array{lifetime:0|positive-int,path:non-falsy-string,domain:string,secure:bool,httponly:bool,samesite:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'session_id' => ['string|false', 'newid='=>'string'], 'session_is_registered' => ['bool', 'name'=>'string'], 'session_module_name' => ['string|false', 'newname='=>'string'], -'session_name' => ['string|false', 'newname='=>'string'], +'session_name' => ['non-falsy-string|false', 'newname='=>'string'], 'session_pgsql_add_error' => ['bool', 'error_level'=>'int', 'error_message='=>'string'], 'session_pgsql_get_error' => ['array', 'with_error_message='=>'bool'], 'session_pgsql_get_field' => ['string'], @@ -10111,19 +9282,19 @@ 'session_register_shutdown' => ['void'], 'session_reset' => ['bool'], 'session_save_path' => ['string|false', 'newname='=>'string'], -'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'string', 'domain='=>'?string', 'secure='=>'bool', 'httponly='=>'bool'], -'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool}'], +'session_set_cookie_params' => ['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,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,37 +9302,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', 'callback'=>'null|callable(int,string,string,int,array):bool|callable(int,string,string,int):bool|callable(int,string,string):bool|callable(int,string):bool', 'error_types='=>'int'], +'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int,array):bool', 'error_types='=>'int'], 'set_exception_handler' => ['null|callable(Throwable):void', 'exception_handler'=>'null|callable(Throwable):void'], 'set_file_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], 'set_include_path' => ['string|false', 'new_include_path'=>'string'], 'set_magic_quotes_runtime' => ['bool', 'new_setting'=>'bool'], 'set_time_limit' => ['bool', 'seconds'=>'int'], -'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setLeftFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setLine' => ['void', 'width'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setlocale' => ['string|false', 'category'=>'int', 'locale'=>'string|null', '...args='=>'string'], 'setlocale\'1' => ['string|false', 'category'=>'int', 'locale'=>'?array'], -'setproctitle' => ['void', 'title'=>'string'], -'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setRightFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setthreadtitle' => ['bool', 'title'=>'string'], 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], -'sha1' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha1_file' => ['string|false', 'filename'=>'string', 'raw_output='=>'bool'], -'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], +'sha1' => ['non-falsy-string&lowercase-string', 'str'=>'string', 'raw_output='=>'bool'], +'sha1_file' => ['(non-falsy-string&lowercase-string)|false', 'filename'=>'string', 'raw_output='=>'bool'], 'shapefileObj::__construct' => ['void', 'filename'=>'string', 'type'=>'int'], 'shapefileObj::addPoint' => ['int', 'point'=>'pointObj'], 'shapefileObj::addShape' => ['int', 'shape'=>'shapeObj'], @@ -10204,7 +9372,7 @@ 'shapeObj::toWkt' => ['string'], 'shapeObj::union' => ['shapeObj', 'shape'=>'shapeObj'], 'shapeObj::within' => ['int', 'shape2'=>'shapeObj'], -'shell_exec' => ['?string', 'cmd'=>'string'], +'shell_exec' => ['string|false|null', 'cmd'=>'string'], 'shm_attach' => ['resource|false', 'key'=>'int', 'memsize='=>'int', 'perm='=>'int'], 'shm_detach' => ['bool', 'shm_identifier'=>'resource'], 'shm_get_var' => ['mixed', 'id'=>'resource', 'variable_key'=>'int'], @@ -10229,16 +9397,16 @@ 'SimpleXMLElement::__get' => ['static', 'name'=>'string'], 'SimpleXMLElement::__toString' => ['string'], 'SimpleXMLElement::addAttribute' => ['void', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], -'SimpleXMLElement::addChild' => ['static', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], +'SimpleXMLElement::addChild' => ['__benevolent', 'name'=>'string', 'value='=>'string|null', 'ns='=>'string|null'], 'SimpleXMLElement::asXML' => ['string|bool', 'filename='=>'string'], -'SimpleXMLElement::attributes' => ['static|null', 'ns='=>'string', 'is_prefix='=>'bool'], -'SimpleXMLElement::children' => ['static', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], +'SimpleXMLElement::attributes' => ['__benevolent', 'ns='=>'string', 'is_prefix='=>'bool'], +'SimpleXMLElement::children' => ['__benevolent', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], 'SimpleXMLElement::count' => ['0|positive-int'], -'SimpleXMLElement::getDocNamespaces' => ['string[]', 'recursive='=>'bool', 'from_root='=>'bool'], +'SimpleXMLElement::getDocNamespaces' => ['string[]|false', 'recursive='=>'bool', 'from_root='=>'bool'], 'SimpleXMLElement::getName' => ['string'], 'SimpleXMLElement::getNamespaces' => ['string[]', 'recursive='=>'bool'], 'SimpleXMLElement::registerXPathNamespace' => ['bool', 'prefix'=>'string', 'ns'=>'string'], -'SimpleXMLElement::xpath' => ['static[]|false', 'path'=>'string'], +'SimpleXMLElement::xpath' => ['static[]|false|null', 'path'=>'string'], 'SimpleXMLIterator::current' => ['SimpleXMLIterator'], 'SimpleXMLIterator::getChildren' => ['SimpleXMLIterator'], 'SimpleXMLIterator::hasChildren' => ['bool'], @@ -10250,39 +9418,48 @@ 'sinh' => ['float', 'number'=>'float'], 'sizeof' => ['int', 'var'=>'Countable|array', 'mode='=>'int'], 'sleep' => ['int|false', 'seconds'=>'int'], -'snmp2_get' => ['string|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp2_getnext' => ['string|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp2_real_walk' => ['array|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp2_set' => ['bool', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'type'=>'string', 'value'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp2_walk' => ['array|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp3_get' => ['string|false', 'host'=>'string', 'sec_name'=>'string', 'sec_level'=>'string', 'auth_protocol'=>'string', 'auth_passphrase'=>'string', 'priv_protocol'=>'string', 'priv_passphrase'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp3_getnext' => ['string|false', 'host'=>'string', 'sec_name'=>'string', 'sec_level'=>'string', 'auth_protocol'=>'string', 'auth_passphrase'=>'string', 'priv_protocol'=>'string', 'priv_passphrase'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp3_real_walk' => ['array|false', 'host'=>'string', 'sec_name'=>'string', 'sec_level'=>'string', 'auth_protocol'=>'string', 'auth_passphrase'=>'string', 'priv_protocol'=>'string', 'priv_passphrase'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp3_set' => ['bool', 'host'=>'string', 'sec_name'=>'string', 'sec_level'=>'string', 'auth_protocol'=>'string', 'auth_passphrase'=>'string', 'priv_protocol'=>'string', 'priv_passphrase'=>'string', 'object_id'=>'string', 'type'=>'string', 'value'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmp3_walk' => ['array|false', 'host'=>'string', 'sec_name'=>'string', 'sec_level'=>'string', 'auth_protocol'=>'string', 'auth_passphrase'=>'string', 'priv_protocol'=>'string', 'priv_passphrase'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'SNMP::__construct' => ['void', 'version'=>'int', 'hostname'=>'string', 'community'=>'string', 'timeout='=>'int', 'retries='=>'int'], +'snmp2_get' => ['string|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_get\'1' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_getnext' => ['string|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_real_walk' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_set' => ['bool', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'type'=>'string', 'value'=>'string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_set\'1' => ['bool', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-array', 'type'=>'string|non-empty-array', 'value'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp2_walk' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_get' => ['string|false', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_get\'1' => ['array|false', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_getnext' => ['string|false', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_real_walk' => ['array|false', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_set' => ['bool', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-string', 'type'=>'string', 'value'=>'string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_set\'1' => ['bool', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'array', 'type'=>'string|non-empty-array', 'value'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmp3_walk' => ['array|false', 'hostname'=>'string', 'sec_name'=>'string', 'sec_level'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'auth_protocol'=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'auth_passphrase'=>'string', 'priv_protocol'=>'\'AES\'|\'AES128\'|\'DES\'', 'priv_passphrase'=>'string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'SNMP::__construct' => ['void', 'version'=>'int', 'hostname'=>'string', 'community'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], 'SNMP::close' => ['bool'], -'SNMP::get' => ['array|string|false', 'object_id'=>'string|array', 'preserve_keys='=>'bool'], +'SNMP::get' => ['array|false', 'objectId'=>'string[]', 'preserveKeys='=>'bool'], +'SNMP::get\'1' => ['string|false', 'objectId'=>'string', 'preserveKeys='=>'bool'], 'SNMP::getErrno' => ['int'], 'SNMP::getError' => ['string'], -'SNMP::getnext' => ['string|array|false', 'object_id'=>'string|array'], -'SNMP::set' => ['bool', 'object_id'=>'string|array', 'type'=>'string|array', 'value'=>'mixed'], -'SNMP::setSecurity' => ['bool', 'sec_level'=>'string', 'auth_protocol='=>'string', 'auth_passphrase='=>'string', 'priv_protocol='=>'string', 'priv_passphrase='=>'string', 'contextname='=>'string', 'contextengineid='=>'string'], -'SNMP::walk' => ['array|false', 'object_id'=>'string', 'suffix_as_key='=>'bool', 'non_repeaters='=>'int', 'max_repetitions='=>'int'], +'SNMP::getnext' => ['array|false', 'objectId'=>'string[]'], +'SNMP::getnext\'1' => ['string|false', 'objectId'=>'string'], +'SNMP::set' => ['bool', 'objectId'=>'string[]', 'type'=>'string[]|string', 'value'=>'mixed[]'], +'SNMP::set\'1' => ['bool', 'objectId'=>'string', 'type'=>'string', 'value'=>'string'], +'SNMP::setSecurity' => ['bool', 'securityLevel'=>'\'authPriv\'|\'authNoPriv\'|\'noAuthNoPriv\'', 'authProtocol='=>'\'SHA\'|\'SHA256\'|\'SHA512\'|\'MD5\'', 'authPassphrase='=>'string', 'privacyProtocol='=>'\'AES\'|\'AES128\'|\'DES\'', 'privacyPassphrase='=>'string', 'contextName='=>'string', 'contextEngineId='=>'string'], +'SNMP::walk' => ['array|false', 'objectId'=>'string', 'suffixAsKey='=>'bool', 'maxRepetitions='=>'int<-1,max>', 'nonRepeaters='=>'int<-1,max>'], 'snmp_get_quick_print' => ['bool'], 'snmp_get_valueretrieval' => ['int'], 'snmp_read_mib' => ['bool', 'filename'=>'string'], -'snmp_set_enum_print' => ['bool', 'enum_print'=>'int'], -'snmp_set_oid_numeric_print' => ['void', 'oid_format'=>'int'], -'snmp_set_oid_output_format' => ['bool', 'oid_format'=>'int'], -'snmp_set_quick_print' => ['bool', 'quick_print'=>'int'], -'snmp_set_valueretrieval' => ['bool', 'method='=>'int'], -'snmpget' => ['string|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmpgetnext' => ['string|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'snmprealwalk' => ['array|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'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'], +'snmp_set_enum_print' => ['true', 'enable'=>'bool'], +'snmp_set_oid_numeric_print' => ['true', 'format'=>'int'], +'snmp_set_oid_output_format' => ['true', 'format'=>'int'], +'snmp_set_quick_print' => ['true', 'enable'=>'bool'], +'snmp_set_valueretrieval' => ['true', 'method='=>'int'], +'snmpget' => ['string|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpget\'1' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpgetnext' => ['string|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmprealwalk' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpset' => ['bool', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'type'=>'string', 'value'=>'string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpset\'1' => ['bool', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-array', 'type'=>'string|non-empty-array', 'value'=>'non-empty-array', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpwalk' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], +'snmpwalkoid' => ['array|false', 'hostname'=>'string', 'community'=>'non-empty-string', 'object_id'=>'non-empty-string', 'timeout='=>'int<-1,max>', 'retries='=>'int<-1,max>'], 'SoapClient::__call' => ['mixed', 'function_name'=>'string', 'arguments'=>'array'], 'SoapClient::__construct' => ['void', 'wsdl'=>'mixed', 'options='=>'array|null'], 'SoapClient::__doRequest' => ['string|null', 'request'=>'string', 'location'=>'string', 'action'=>'string', 'version'=>'int', 'one_way='=>'int'], @@ -10298,7 +9475,7 @@ 'SoapClient::__setSoapHeaders' => ['bool', 'soapheaders='=>''], 'SoapClient::__soapCall' => ['mixed', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'], 'SoapClient::SoapClient' => ['object', 'wsdl'=>'mixed', 'options='=>'array|null'], -'SoapFault::__construct' => ['void', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'string'], +'SoapFault::__construct' => ['void', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'mixed', 'faultname='=>'string', 'headerfault='=>'mixed'], 'SoapFault::__toString' => ['string'], 'SoapFault::SoapFault' => ['object', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'string'], 'SoapHeader::__construct' => ['void', 'namespace'=>'string', 'name'=>'string', 'data='=>'mixed', 'mustunderstand='=>'bool', 'actor='=>'string'], @@ -10342,7 +9519,7 @@ 'socket_recv' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_recvfrom' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int', '&w_name'=>'string', '&w_port='=>'int'], 'socket_recvmsg' => ['int|false', 'socket'=>'resource', '&w_message'=>'string', 'flags='=>'int'], -'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], +'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'socket_send' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_sendmsg' => ['int|false', 'socket'=>'resource', 'message'=>'array', 'flags'=>'int'], 'socket_sendto' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int', 'addr'=>'string', 'port='=>'int'], @@ -10446,11 +9623,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'], @@ -10480,18 +9657,18 @@ 'sodium_crypto_secretstream_xchacha20poly1305_rekey' => ['void', 'state'=>'string'], 'sodium_crypto_shorthash' => ['string', 'message'=>'string', 'key'=>'string'], 'sodium_crypto_shorthash_keygen' => ['string'], -'sodium_crypto_sign' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_detached' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['string', 'ed25519pk'=>'string'], -'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['string', 'ed25519sk'=>'string'], -'sodium_crypto_sign_keypair' => ['string'], -'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['string', 'secret_key'=>'string', 'public_key'=>'string'], -'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'string'], -'sodium_crypto_sign_publickey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_publickey_from_secretkey' => ['string', 'secretkey'=>'string'], -'sodium_crypto_sign_secretkey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_seed_keypair' => ['string', 'seed'=>'string'], -'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'string', 'message'=>'string', 'publickey'=>'string'], +'sodium_crypto_sign' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_detached' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['non-empty-string', 'ed25519pk'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['non-empty-string', 'ed25519sk'=>'non-empty-string'], +'sodium_crypto_sign_keypair' => ['non-empty-string'], +'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['non-empty-string', 'secret_key'=>'non-empty-string', 'public_key'=>'non-empty-string'], +'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'non-empty-string'], +'sodium_crypto_sign_publickey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_publickey_from_secretkey' => ['non-empty-string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_secretkey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_seed_keypair' => ['non-empty-string', 'seed'=>'non-empty-string'], +'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'non-empty-string', 'message'=>'string', 'publickey'=>'non-empty-string'], 'sodium_crypto_stream' => ['string', 'length'=>'int', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_stream_keygen' => ['string'], 'sodium_crypto_stream_xor' => ['string', 'message'=>'string', 'nonce'=>'string', 'key'=>'string'], @@ -10509,452 +9686,41 @@ 'sodium_version_string' => ['string'], 'solid_fetch_prev' => ['bool', 'result_id'=>''], 'solr_get_version' => ['string'], -'SolrClient::__construct' => ['void', 'clientOptions'=>'array'], -'SolrClient::__destruct' => [''], -'SolrClient::addDocument' => ['SolrUpdateResponse', 'doc'=>'solrinputdocument', 'allowdups='=>'bool', 'commitwithin='=>'int'], -'SolrClient::addDocuments' => ['SolrUpdateResponse', 'docs'=>'array', 'allowdups='=>'bool', 'commitwithin='=>'int'], -'SolrClient::commit' => ['SolrUpdateResponse', 'maxsegments='=>'int', 'waitflush='=>'bool', 'waitsearcher='=>'bool'], -'SolrClient::deleteById' => ['SolrUpdateResponse', 'id'=>'string'], -'SolrClient::deleteByIds' => ['SolrUpdateResponse', 'ids'=>'array'], -'SolrClient::deleteByQueries' => ['SolrUpdateResponse', 'queries'=>'array'], -'SolrClient::deleteByQuery' => ['SolrUpdateResponse', 'query'=>'string'], -'SolrClient::getById' => ['SolrQueryResponse', 'id'=>'string'], -'SolrClient::getByIds' => ['SolrQueryResponse', 'ids'=>'array'], -'SolrClient::getDebug' => ['string'], -'SolrClient::getOptions' => ['array'], -'SolrClient::optimize' => ['SolrUpdateResponse', 'maxsegments='=>'int', 'waitflush='=>'bool', 'waitsearcher='=>'bool'], -'SolrClient::ping' => ['SolrPingResponse'], -'SolrClient::query' => ['SolrQueryResponse', 'query'=>'solrparams'], -'SolrClient::request' => ['SolrUpdateResponse', 'raw_request'=>'string'], -'SolrClient::rollback' => ['SolrUpdateResponse'], 'SolrClient::setResponseWriter' => ['void', 'responsewriter'=>'string'], -'SolrClient::setServlet' => ['bool', 'type'=>'int', 'value'=>'string'], -'SolrClient::system' => ['void'], -'SolrClient::threads' => ['void'], -'SolrClientException::getInternalInfo' => ['array'], -'SolrCollapseFunction::__toString' => ['string'], -'SolrCollapseFunction::getField' => ['string'], -'SolrCollapseFunction::getHint' => ['string'], -'SolrCollapseFunction::getMax' => ['string'], -'SolrCollapseFunction::getMin' => ['string'], -'SolrCollapseFunction::getNullPolicy' => ['string'], -'SolrCollapseFunction::getSize' => ['int'], -'SolrCollapseFunction::setField' => ['SolrCollapseFunction', 'fieldName'=>'string'], -'SolrCollapseFunction::setHint' => ['SolrCollapseFunction', 'hint'=>'string'], -'SolrCollapseFunction::setMax' => ['SolrCollapseFunction', 'max'=>'string'], -'SolrCollapseFunction::setMin' => ['SolrCollapseFunction', 'min'=>'string'], -'SolrCollapseFunction::setNullPolicy' => ['SolrCollapseFunction', 'nullPolicy'=>'string'], -'SolrCollapseFunction::setSize' => ['SolrCollapseFunction', 'size'=>'int'], -'SolrDisMaxQuery::__construct' => ['void', 'q='=>'string'], 'SolrDisMaxQuery::addBigramPhraseField' => ['SolrDisMaxQuery', 'field'=>'string', 'boost'=>'string', 'slop='=>'string'], 'SolrDisMaxQuery::addBoostQuery' => ['SolrDisMaxQuery', 'field'=>'string', 'value'=>'string', 'boost='=>'string'], -'SolrDisMaxQuery::addExpandFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrDisMaxQuery::addExpandSortField' => ['SolrQuery', 'field'=>'string', 'order'=>'string'], -'SolrDisMaxQuery::addFacetDateField' => ['SolrQuery', 'dateField'=>'string'], -'SolrDisMaxQuery::addFacetDateOther' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::addFacetField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::addFacetQuery' => ['SolrQuery', 'facetQuery'=>'string'], -'SolrDisMaxQuery::addField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::addFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrDisMaxQuery::addGroupField' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::addGroupFunction' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::addGroupQuery' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::addGroupSortField' => ['SolrQuery', 'field'=>'string', 'order'=>'int'], -'SolrDisMaxQuery::addHighlightField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::addMltField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::addMltQueryField' => ['SolrQuery', 'field'=>'string', 'boost'=>'float'], -'SolrDisMaxQuery::addParam' => ['SolrParams', 'name'=>'string', 'value'=>'string'], 'SolrDisMaxQuery::addPhraseField' => ['SolrDisMaxQuery', 'field'=>'string', 'boost'=>'string', 'slop='=>'string'], 'SolrDisMaxQuery::addQueryField' => ['SolrDisMaxQuery', 'field'=>'string', 'boost='=>'string'], -'SolrDisMaxQuery::addSortField' => ['SolrQuery', 'field'=>'string', 'order='=>'int'], -'SolrDisMaxQuery::addStatsFacet' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::addStatsField' => ['SolrQuery', 'field'=>'string'], 'SolrDisMaxQuery::addTrigramPhraseField' => ['SolrDisMaxQuery', 'field'=>'string', 'boost'=>'string', 'slop='=>'string'], -'SolrDisMaxQuery::addUserField' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::collapse' => ['SolrQuery', 'collapseFunction'=>'SolrCollapseFunction'], -'SolrDisMaxQuery::get' => ['mixed', 'param_name'=>'string'], -'SolrDisMaxQuery::getExpand' => ['bool'], -'SolrDisMaxQuery::getExpandFilterQueries' => ['array'], -'SolrDisMaxQuery::getExpandQuery' => ['array'], -'SolrDisMaxQuery::getExpandRows' => ['int'], -'SolrDisMaxQuery::getExpandSortFields' => ['array'], -'SolrDisMaxQuery::getFacet' => ['bool'], -'SolrDisMaxQuery::getFacetDateEnd' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetDateFields' => ['array'], -'SolrDisMaxQuery::getFacetDateGap' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetDateHardEnd' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetDateOther' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetDateStart' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetFields' => ['array'], -'SolrDisMaxQuery::getFacetLimit' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetMethod' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetMinCount' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetMissing' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetOffset' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetPrefix' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getFacetQueries' => ['string'], -'SolrDisMaxQuery::getFacetSort' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getFields' => ['string'], -'SolrDisMaxQuery::getFilterQueries' => ['string'], -'SolrDisMaxQuery::getGroup' => ['bool'], -'SolrDisMaxQuery::getGroupCachePercent' => ['int'], -'SolrDisMaxQuery::getGroupFacet' => ['bool'], -'SolrDisMaxQuery::getGroupFields' => ['array'], -'SolrDisMaxQuery::getGroupFormat' => ['string'], -'SolrDisMaxQuery::getGroupFunctions' => ['array'], -'SolrDisMaxQuery::getGroupLimit' => ['int'], -'SolrDisMaxQuery::getGroupMain' => ['bool'], -'SolrDisMaxQuery::getGroupNGroups' => ['bool'], -'SolrDisMaxQuery::getGroupOffset' => ['bool'], -'SolrDisMaxQuery::getGroupQueries' => ['array'], -'SolrDisMaxQuery::getGroupSortFields' => ['array'], -'SolrDisMaxQuery::getGroupTruncate' => ['bool'], -'SolrDisMaxQuery::getHighlight' => ['bool'], -'SolrDisMaxQuery::getHighlightAlternateField' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightFields' => ['array'], -'SolrDisMaxQuery::getHighlightFormatter' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightFragmenter' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightFragsize' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightHighlightMultiTerm' => ['bool'], -'SolrDisMaxQuery::getHighlightMaxAlternateFieldLength' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightMaxAnalyzedChars' => ['int'], -'SolrDisMaxQuery::getHighlightMergeContiguous' => ['bool', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightRegexMaxAnalyzedChars' => ['int'], -'SolrDisMaxQuery::getHighlightRegexPattern' => ['string'], -'SolrDisMaxQuery::getHighlightRegexSlop' => ['float'], -'SolrDisMaxQuery::getHighlightRequireFieldMatch' => ['bool'], -'SolrDisMaxQuery::getHighlightSimplePost' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightSimplePre' => ['string', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightSnippets' => ['int', 'field_override'=>'string'], -'SolrDisMaxQuery::getHighlightUsePhraseHighlighter' => ['bool'], -'SolrDisMaxQuery::getMlt' => ['bool'], -'SolrDisMaxQuery::getMltBoost' => ['bool'], -'SolrDisMaxQuery::getMltCount' => ['int'], -'SolrDisMaxQuery::getMltFields' => ['array'], -'SolrDisMaxQuery::getMltMaxNumQueryTerms' => ['int'], -'SolrDisMaxQuery::getMltMaxNumTokens' => ['int'], -'SolrDisMaxQuery::getMltMaxWordLength' => ['int'], -'SolrDisMaxQuery::getMltMinDocFrequency' => ['int'], -'SolrDisMaxQuery::getMltMinTermFrequency' => ['int'], -'SolrDisMaxQuery::getMltMinWordLength' => ['int'], -'SolrDisMaxQuery::getMltQueryFields' => ['array'], -'SolrDisMaxQuery::getParam' => ['mixed', 'param_name'=>'string'], -'SolrDisMaxQuery::getParams' => ['array'], -'SolrDisMaxQuery::getPreparedParams' => ['array'], -'SolrDisMaxQuery::getQuery' => ['string'], -'SolrDisMaxQuery::getRows' => ['int'], -'SolrDisMaxQuery::getSortFields' => ['array'], -'SolrDisMaxQuery::getStart' => ['int'], -'SolrDisMaxQuery::getStats' => ['bool'], -'SolrDisMaxQuery::getStatsFacets' => ['array'], -'SolrDisMaxQuery::getStatsFields' => ['array'], -'SolrDisMaxQuery::getTerms' => ['bool'], -'SolrDisMaxQuery::getTermsField' => ['string'], -'SolrDisMaxQuery::getTermsIncludeLowerBound' => ['bool'], -'SolrDisMaxQuery::getTermsIncludeUpperBound' => ['bool'], -'SolrDisMaxQuery::getTermsLimit' => ['int'], -'SolrDisMaxQuery::getTermsLowerBound' => ['string'], -'SolrDisMaxQuery::getTermsMaxCount' => ['int'], -'SolrDisMaxQuery::getTermsMinCount' => ['int'], -'SolrDisMaxQuery::getTermsPrefix' => ['string'], -'SolrDisMaxQuery::getTermsReturnRaw' => ['bool'], -'SolrDisMaxQuery::getTermsSort' => ['int'], -'SolrDisMaxQuery::getTermsUpperBound' => ['string'], -'SolrDisMaxQuery::getTimeAllowed' => ['int'], -'SolrDisMaxQuery::removeBigramPhraseField' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeBoostQuery' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeExpandFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrDisMaxQuery::removeExpandSortField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeFacetDateField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeFacetDateOther' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::removeFacetField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeFacetQuery' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::removeField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrDisMaxQuery::removeHighlightField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeMltField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeMltQueryField' => ['SolrQuery', 'queryField'=>'string'], -'SolrDisMaxQuery::removePhraseField' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeQueryField' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeSortField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeStatsFacet' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::removeStatsField' => ['SolrQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeTrigramPhraseField' => ['SolrDisMaxQuery', 'field'=>'string'], -'SolrDisMaxQuery::removeUserField' => ['SolrDisMaxQuery', 'field'=>'string'], 'SolrDisMaxQuery::serialize' => ['string'], -'SolrDisMaxQuery::set' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrDisMaxQuery::setBigramPhraseFields' => ['SolrDisMaxQuery', 'fields'=>'string'], -'SolrDisMaxQuery::setBigramPhraseSlop' => ['SolrDisMaxQuery', 'slop'=>'string'], -'SolrDisMaxQuery::setBoostFunction' => ['SolrDisMaxQuery', 'function'=>'string'], -'SolrDisMaxQuery::setBoostQuery' => ['SolrDisMaxQuery', 'q'=>'string'], -'SolrDisMaxQuery::setEchoHandler' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setEchoParams' => ['SolrQuery', 'type'=>'string'], -'SolrDisMaxQuery::setExpand' => ['SolrQuery', 'value'=>'bool'], -'SolrDisMaxQuery::setExpandQuery' => ['SolrQuery', 'q'=>'string'], -'SolrDisMaxQuery::setExpandRows' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setExplainOther' => ['SolrQuery', 'query'=>'string'], -'SolrDisMaxQuery::setFacet' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setFacetDateEnd' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetDateGap' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetDateHardEnd' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetDateStart' => ['SolrQuery', 'value'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetEnumCacheMinDefaultFrequency' => ['SolrQuery', 'frequency'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetLimit' => ['SolrQuery', 'limit'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetMethod' => ['SolrQuery', 'method'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetMinCount' => ['SolrQuery', 'mincount'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetMissing' => ['SolrQuery', 'flag'=>'bool', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetOffset' => ['SolrQuery', 'offset'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetPrefix' => ['SolrQuery', 'prefix'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setFacetSort' => ['SolrQuery', 'facetSort'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setGroup' => ['SolrQuery', 'value'=>'bool'], -'SolrDisMaxQuery::setGroupCachePercent' => ['SolrQuery', 'percent'=>'int'], -'SolrDisMaxQuery::setGroupFacet' => ['SolrQuery', 'value'=>'bool'], -'SolrDisMaxQuery::setGroupFormat' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::setGroupLimit' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setGroupMain' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::setGroupNGroups' => ['SolrQuery', 'value'=>'bool'], -'SolrDisMaxQuery::setGroupOffset' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setGroupTruncate' => ['SolrQuery', 'value'=>'bool'], -'SolrDisMaxQuery::setHighlight' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setHighlightAlternateField' => ['SolrQuery', 'field'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightFormatter' => ['SolrQuery', 'formatter'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightFragmenter' => ['SolrQuery', 'fragmenter'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightFragsize' => ['SolrQuery', 'size'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightHighlightMultiTerm' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setHighlightMaxAlternateFieldLength' => ['SolrQuery', 'fieldLength'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightMaxAnalyzedChars' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setHighlightMergeContiguous' => ['SolrQuery', 'flag'=>'bool', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightRegexMaxAnalyzedChars' => ['SolrQuery', 'maxAnalyzedChars'=>'int'], -'SolrDisMaxQuery::setHighlightRegexPattern' => ['SolrQuery', 'value'=>'string'], -'SolrDisMaxQuery::setHighlightRegexSlop' => ['SolrQuery', 'factor'=>'float'], -'SolrDisMaxQuery::setHighlightRequireFieldMatch' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setHighlightSimplePost' => ['SolrQuery', 'simplePost'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightSimplePre' => ['SolrQuery', 'simplePre'=>'string', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightSnippets' => ['SolrQuery', 'value'=>'int', 'field_override'=>'string'], -'SolrDisMaxQuery::setHighlightUsePhraseHighlighter' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setMinimumMatch' => ['SolrDisMaxQuery', 'value'=>'string'], -'SolrDisMaxQuery::setMlt' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setMltBoost' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setMltCount' => ['SolrQuery', 'count'=>'int'], -'SolrDisMaxQuery::setMltMaxNumQueryTerms' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setMltMaxNumTokens' => ['SolrQuery', 'value'=>'int'], -'SolrDisMaxQuery::setMltMaxWordLength' => ['SolrQuery', 'maxWordLength'=>'int'], -'SolrDisMaxQuery::setMltMinDocFrequency' => ['SolrQuery', 'minDocFrequency'=>'int'], -'SolrDisMaxQuery::setMltMinTermFrequency' => ['SolrQuery', 'minTermFrequency'=>'int'], -'SolrDisMaxQuery::setMltMinWordLength' => ['SolrQuery', 'minWordLength'=>'int'], -'SolrDisMaxQuery::setOmitHeader' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setParam' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrDisMaxQuery::setPhraseFields' => ['SolrDisMaxQuery', 'fields'=>'string'], -'SolrDisMaxQuery::setPhraseSlop' => ['SolrDisMaxQuery', 'slop'=>'string'], -'SolrDisMaxQuery::setQuery' => ['SolrQuery', 'query'=>'string'], -'SolrDisMaxQuery::setQueryAlt' => ['SolrDisMaxQuery', 'q'=>'string'], -'SolrDisMaxQuery::setQueryPhraseSlop' => ['SolrDisMaxQuery', 'slop'=>'string'], -'SolrDisMaxQuery::setRows' => ['SolrQuery', 'rows'=>'int'], -'SolrDisMaxQuery::setShowDebugInfo' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setStart' => ['SolrQuery', 'start'=>'int'], -'SolrDisMaxQuery::setStats' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setTerms' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setTermsField' => ['SolrQuery', 'fieldname'=>'string'], -'SolrDisMaxQuery::setTermsIncludeLowerBound' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setTermsIncludeUpperBound' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setTermsLimit' => ['SolrQuery', 'limit'=>'int'], -'SolrDisMaxQuery::setTermsLowerBound' => ['SolrQuery', 'lowerBound'=>'string'], -'SolrDisMaxQuery::setTermsMaxCount' => ['SolrQuery', 'frequency'=>'int'], -'SolrDisMaxQuery::setTermsMinCount' => ['SolrQuery', 'frequency'=>'int'], -'SolrDisMaxQuery::setTermsPrefix' => ['SolrQuery', 'prefix'=>'string'], -'SolrDisMaxQuery::setTermsReturnRaw' => ['SolrQuery', 'flag'=>'bool'], -'SolrDisMaxQuery::setTermsSort' => ['SolrQuery', 'sortType'=>'int'], -'SolrDisMaxQuery::setTermsUpperBound' => ['SolrQuery', 'upperBound'=>'string'], -'SolrDisMaxQuery::setTieBreaker' => ['SolrDisMaxQuery', 'tieBreaker'=>'string'], -'SolrDisMaxQuery::setTimeAllowed' => ['SolrQuery', 'timeAllowed'=>'int'], -'SolrDisMaxQuery::setTrigramPhraseFields' => ['SolrDisMaxQuery', 'fields'=>'string'], -'SolrDisMaxQuery::setTrigramPhraseSlop' => ['SolrDisMaxQuery', 'slop'=>'string'], -'SolrDisMaxQuery::setUserFields' => ['SolrDisMaxQuery', 'fields'=>'string'], -'SolrDisMaxQuery::toString' => ['string', 'url_encode='=>'bool|false'], 'SolrDisMaxQuery::unserialize' => ['void', 'serialized'=>'string'], -'SolrDisMaxQuery::useDisMaxQueryParser' => ['SolrDisMaxQuery'], -'SolrDisMaxQuery::useEDisMaxQueryParser' => ['SolrDisMaxQuery'], -'SolrDocument::__clone' => ['void'], -'SolrDocument::__construct' => ['void'], -'SolrDocument::__destruct' => [''], -'SolrDocument::__get' => ['SolrDocumentField', 'fieldname'=>'string'], -'SolrDocument::__isset' => ['bool', 'fieldname'=>'string'], -'SolrDocument::__set' => ['bool', 'fieldname'=>'string', 'fieldvalue'=>'string'], 'SolrDocument::__unset' => ['bool', 'fieldname'=>'string'], -'SolrDocument::addField' => ['bool', 'fieldname'=>'string', 'fieldvalue'=>'string'], -'SolrDocument::clear' => ['bool'], -'SolrDocument::current' => ['SolrDocumentField'], -'SolrDocument::deleteField' => ['bool', 'fieldname'=>'string'], -'SolrDocument::fieldExists' => ['bool', 'fieldname'=>'string'], -'SolrDocument::getChildDocuments' => ['array'], -'SolrDocument::getChildDocumentsCount' => ['int'], -'SolrDocument::getField' => ['SolrDocumentField', 'fieldname'=>'string'], -'SolrDocument::getFieldCount' => ['int'], -'SolrDocument::getFieldNames' => ['array'], -'SolrDocument::getInputDocument' => ['SolrInputDocument'], -'SolrDocument::hasChildDocuments' => ['bool'], -'SolrDocument::key' => ['string'], -'SolrDocument::merge' => ['bool', 'sourcedoc'=>'solrdocument', 'overwrite='=>'bool'], +'SolrDocument::getField' => ['__benevolent', 'fieldname'=>'string'], +'SolrDocument::getFieldCount' => ['__benevolent'], +'SolrDocument::getFieldNames' => ['__benevolent'], +'SolrDocument::getInputDocument' => ['__benevolent'], 'SolrDocument::next' => ['void'], -'SolrDocument::offsetExists' => ['bool', 'fieldname'=>'string'], -'SolrDocument::offsetGet' => ['SolrDocumentField', 'fieldname'=>'string'], -'SolrDocument::offsetSet' => ['void', 'fieldname'=>'string', 'fieldvalue'=>'string'], -'SolrDocument::offsetUnset' => ['void', 'fieldname'=>'string'], -'SolrDocument::reset' => ['bool'], -'SolrDocument::rewind' => ['void'], -'SolrDocument::serialize' => ['string'], -'SolrDocument::sort' => ['bool', 'sortorderby'=>'int', 'sortdirection='=>'int'], -'SolrDocument::toArray' => ['array'], 'SolrDocument::unserialize' => ['void', 'serialized'=>'string'], -'SolrDocument::valid' => ['bool'], -'SolrDocumentField::__construct' => ['void'], -'SolrDocumentField::__destruct' => [''], -'SolrException::__clone' => ['void'], -'SolrException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], -'SolrException::__toString' => ['string'], -'SolrException::__wakeup' => ['void'], -'SolrException::getCode' => ['int'], -'SolrException::getFile' => ['string'], -'SolrException::getInternalInfo' => ['array'], -'SolrException::getLine' => ['int'], -'SolrException::getMessage' => ['string'], -'SolrException::getPrevious' => ['Exception|Throwable'], -'SolrException::getTrace' => ['array'], -'SolrException::getTraceAsString' => ['string'], -'SolrGenericResponse::__construct' => ['void'], -'SolrGenericResponse::__destruct' => [''], -'SolrGenericResponse::getDigestedResponse' => ['string'], -'SolrGenericResponse::getHttpStatus' => ['int'], -'SolrGenericResponse::getHttpStatusMessage' => ['string'], -'SolrGenericResponse::getRawRequest' => ['string'], -'SolrGenericResponse::getRawRequestHeaders' => ['string'], -'SolrGenericResponse::getRawResponse' => ['string'], -'SolrGenericResponse::getRawResponseHeaders' => ['string'], -'SolrGenericResponse::getRequestUrl' => ['string'], -'SolrGenericResponse::getResponse' => ['SolrObject'], -'SolrGenericResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], -'SolrGenericResponse::success' => ['bool'], -'SolrIllegalArgumentException::__clone' => ['void'], -'SolrIllegalArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], -'SolrIllegalArgumentException::__toString' => ['string'], -'SolrIllegalArgumentException::__wakeup' => ['void'], -'SolrIllegalArgumentException::getCode' => ['int'], -'SolrIllegalArgumentException::getFile' => ['string'], -'SolrIllegalArgumentException::getInternalInfo' => ['array'], -'SolrIllegalArgumentException::getLine' => ['int'], -'SolrIllegalArgumentException::getMessage' => ['string'], -'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalArgumentException::getTrace' => ['array'], -'SolrIllegalArgumentException::getTraceAsString' => ['string'], -'SolrIllegalOperationException::__clone' => ['void'], -'SolrIllegalOperationException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], -'SolrIllegalOperationException::__toString' => ['string'], -'SolrIllegalOperationException::__wakeup' => ['void'], -'SolrIllegalOperationException::getCode' => ['int'], -'SolrIllegalOperationException::getFile' => ['string'], -'SolrIllegalOperationException::getInternalInfo' => ['array'], -'SolrIllegalOperationException::getLine' => ['int'], -'SolrIllegalOperationException::getMessage' => ['string'], -'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalOperationException::getTrace' => ['array'], -'SolrIllegalOperationException::getTraceAsString' => ['string'], -'SolrInputDocument::__clone' => ['void'], -'SolrInputDocument::__construct' => ['void'], -'SolrInputDocument::__destruct' => [''], +'SolrException::getTrace' => ['list\',args?:list,object?:object}>'], 'SolrInputDocument::addChildDocument' => ['void', 'child'=>'SolrInputDocument'], 'SolrInputDocument::addChildDocuments' => ['void', 'docs'=>'array'], -'SolrInputDocument::addField' => ['bool', 'fieldname'=>'string', 'fieldvalue'=>'string', 'fieldboostvalue='=>'float'], -'SolrInputDocument::clear' => ['bool'], -'SolrInputDocument::deleteField' => ['bool', 'fieldname'=>'string'], -'SolrInputDocument::fieldExists' => ['bool', 'fieldname'=>'string'], 'SolrInputDocument::getBoost' => ['float'], -'SolrInputDocument::getChildDocuments' => ['array'], -'SolrInputDocument::getChildDocumentsCount' => ['int'], 'SolrInputDocument::getField' => ['SolrDocumentField', 'fieldname'=>'string'], 'SolrInputDocument::getFieldBoost' => ['float', 'fieldname'=>'string'], 'SolrInputDocument::getFieldCount' => ['int'], 'SolrInputDocument::getFieldNames' => ['array'], 'SolrInputDocument::hasChildDocuments' => ['bool'], -'SolrInputDocument::merge' => ['bool', 'sourcedoc'=>'solrinputdocument', 'overwrite='=>'bool'], -'SolrInputDocument::reset' => ['bool'], -'SolrInputDocument::setBoost' => ['bool', 'documentboostvalue'=>'float'], 'SolrInputDocument::setFieldBoost' => ['bool', 'fieldname'=>'string', 'fieldboostvalue'=>'float'], -'SolrInputDocument::sort' => ['bool', 'sortorderby'=>'int', 'sortdirection='=>'int'], 'SolrInputDocument::toArray' => ['array'], -'SolrModifiableParams::__construct' => ['void'], -'SolrModifiableParams::__destruct' => [''], -'SolrModifiableParams::add' => ['SolrParams', 'name'=>'string', 'value'=>'string'], -'SolrModifiableParams::addParam' => ['SolrParams', 'name'=>'string', 'value'=>'string'], -'SolrModifiableParams::get' => ['mixed', 'param_name'=>'string'], -'SolrModifiableParams::getParam' => ['mixed', 'param_name'=>'string'], -'SolrModifiableParams::getParams' => ['array'], -'SolrModifiableParams::getPreparedParams' => ['array'], -'SolrModifiableParams::serialize' => ['string'], -'SolrModifiableParams::set' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrModifiableParams::setParam' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrModifiableParams::toString' => ['string', 'url_encode='=>'bool|false'], -'SolrModifiableParams::unserialize' => ['void', 'serialized'=>'string'], -'SolrObject::__construct' => ['void'], -'SolrObject::__destruct' => [''], -'SolrObject::getPropertyNames' => ['array'], -'SolrObject::offsetExists' => ['bool', 'property_name'=>'string'], -'SolrObject::offsetGet' => ['mixed', 'property_name'=>'string'], -'SolrObject::offsetSet' => ['void', 'property_name'=>'string', 'property_value'=>'string'], 'SolrObject::offsetUnset' => ['void', 'property_name'=>'string'], -'SolrParams::__construct' => ['void'], 'SolrParams::add' => ['SolrParams', 'name'=>'string', 'value'=>'string'], 'SolrParams::addParam' => ['SolrParams', 'name'=>'string', 'value'=>'string'], -'SolrParams::get' => ['mixed', 'param_name'=>'string'], -'SolrParams::getParam' => ['mixed', 'param_name='=>'string'], -'SolrParams::getParams' => ['array'], -'SolrParams::getPreparedParams' => ['array'], -'SolrParams::serialize' => ['string'], 'SolrParams::set' => ['void', 'name'=>'string', 'value'=>'string'], 'SolrParams::setParam' => ['SolrParams', 'name'=>'string', 'value'=>'string'], 'SolrParams::toString' => ['string', 'url_encode='=>'bool'], -'SolrParams::unserialize' => ['void', 'serialized'=>'string'], -'SolrPingResponse::__construct' => ['void'], -'SolrPingResponse::__destruct' => [''], -'SolrPingResponse::getDigestedResponse' => ['string'], -'SolrPingResponse::getHttpStatus' => ['int'], -'SolrPingResponse::getHttpStatusMessage' => ['string'], -'SolrPingResponse::getRawRequest' => ['string'], -'SolrPingResponse::getRawRequestHeaders' => ['string'], -'SolrPingResponse::getRawResponse' => ['string'], -'SolrPingResponse::getRawResponseHeaders' => ['string'], -'SolrPingResponse::getRequestUrl' => ['string'], -'SolrPingResponse::getResponse' => ['string'], -'SolrPingResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], -'SolrPingResponse::success' => ['bool'], -'SolrQuery::__construct' => ['void', 'q='=>'string'], -'SolrQuery::__destruct' => [''], -'SolrQuery::add' => ['SolrParams', 'name'=>'string', 'value'=>'string'], -'SolrQuery::addExpandFilterQuery' => ['SolrQuery', 'fq'=>'string'], 'SolrQuery::addExpandSortField' => ['SolrQuery', 'field'=>'string', 'order='=>'string'], -'SolrQuery::addFacetDateField' => ['SolrQuery', 'datefield'=>'string'], 'SolrQuery::addFacetDateOther' => ['SolrQuery', 'value'=>'string', 'field_override='=>'string'], -'SolrQuery::addFacetField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::addFacetQuery' => ['SolrQuery', 'facetquery'=>'string'], -'SolrQuery::addField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::addFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrQuery::addGroupField' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::addGroupFunction' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::addGroupQuery' => ['SolrQuery', 'value'=>'string'], 'SolrQuery::addGroupSortField' => ['SolrQuery', 'field'=>'string', 'order='=>'int'], -'SolrQuery::addHighlightField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::addMltField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::addMltQueryField' => ['SolrQuery', 'field'=>'string', 'boost'=>'float'], -'SolrQuery::addParam' => ['SolrParams', 'name'=>'string', 'value'=>'string'], -'SolrQuery::addSortField' => ['SolrQuery', 'field'=>'string', 'order='=>'int'], -'SolrQuery::addStatsFacet' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::addStatsField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::collapse' => ['SolrQuery', 'collapseFunction'=>'SolrCollapseFunction'], -'SolrQuery::get' => ['mixed', 'param_name'=>'string'], -'SolrQuery::getExpand' => ['bool'], -'SolrQuery::getExpandFilterQueries' => ['array'], -'SolrQuery::getExpandQuery' => ['array'], -'SolrQuery::getExpandRows' => ['int'], -'SolrQuery::getExpandSortFields' => ['array'], 'SolrQuery::getFacet' => ['bool'], 'SolrQuery::getFacetDateEnd' => ['string', 'field_override='=>'string'], 'SolrQuery::getFacetDateFields' => ['array'], @@ -10973,20 +9739,7 @@ 'SolrQuery::getFacetSort' => ['int', 'field_override='=>'string'], 'SolrQuery::getFields' => ['array'], 'SolrQuery::getFilterQueries' => ['array'], -'SolrQuery::getGroup' => ['bool'], -'SolrQuery::getGroupCachePercent' => ['int'], -'SolrQuery::getGroupFacet' => ['bool'], -'SolrQuery::getGroupFields' => ['array'], -'SolrQuery::getGroupFormat' => ['string'], -'SolrQuery::getGroupFunctions' => ['array'], -'SolrQuery::getGroupLimit' => ['int'], -'SolrQuery::getGroupMain' => ['bool'], -'SolrQuery::getGroupNGroups' => ['bool'], 'SolrQuery::getGroupOffset' => ['int'], -'SolrQuery::getGroupQueries' => ['array'], -'SolrQuery::getGroupSortFields' => ['array'], -'SolrQuery::getGroupTruncate' => ['bool'], -'SolrQuery::getHighlight' => ['bool'], 'SolrQuery::getHighlightAlternateField' => ['string', 'field_override='=>'string'], 'SolrQuery::getHighlightFields' => ['array'], 'SolrQuery::getHighlightFormatter' => ['string', 'field_override='=>'string'], @@ -11038,29 +9791,8 @@ 'SolrQuery::getTermsSort' => ['int'], 'SolrQuery::getTermsUpperBound' => ['string'], 'SolrQuery::getTimeAllowed' => ['int'], -'SolrQuery::removeExpandFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrQuery::removeExpandSortField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeFacetDateField' => ['SolrQuery', 'field'=>'string'], 'SolrQuery::removeFacetDateOther' => ['SolrQuery', 'value'=>'string', 'field_override='=>'string'], -'SolrQuery::removeFacetField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeFacetQuery' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::removeField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeFilterQuery' => ['SolrQuery', 'fq'=>'string'], -'SolrQuery::removeHighlightField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeMltField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeMltQueryField' => ['SolrQuery', 'queryfield'=>'string'], -'SolrQuery::removeSortField' => ['SolrQuery', 'field'=>'string'], -'SolrQuery::removeStatsFacet' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::removeStatsField' => ['SolrQuery', 'field'=>'string'], 'SolrQuery::serialize' => ['string'], -'SolrQuery::set' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrQuery::setEchoHandler' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setEchoParams' => ['SolrQuery', 'type'=>'string'], -'SolrQuery::setExpand' => ['SolrQuery', 'value'=>'bool'], -'SolrQuery::setExpandQuery' => ['SolrQuery', 'q'=>'string'], -'SolrQuery::setExpandRows' => ['SolrQuery', 'value'=>'int'], -'SolrQuery::setExplainOther' => ['SolrQuery', 'query'=>'string'], -'SolrQuery::setFacet' => ['SolrQuery', 'flag'=>'bool'], 'SolrQuery::setFacetDateEnd' => ['SolrQuery', 'value'=>'string', 'field_override='=>'string'], 'SolrQuery::setFacetDateGap' => ['SolrQuery', 'value'=>'string', 'field_override='=>'string'], 'SolrQuery::setFacetDateHardEnd' => ['SolrQuery', 'value'=>'bool', 'field_override='=>'string'], @@ -11073,112 +9805,16 @@ 'SolrQuery::setFacetOffset' => ['SolrQuery', 'offset'=>'int', 'field_override='=>'string'], 'SolrQuery::setFacetPrefix' => ['SolrQuery', 'prefix'=>'string', 'field_override='=>'string'], 'SolrQuery::setFacetSort' => ['SolrQuery', 'facetsort'=>'int', 'field_override='=>'string'], -'SolrQuery::setGroup' => ['SolrQuery', 'value'=>'bool'], -'SolrQuery::setGroupCachePercent' => ['SolrQuery', 'percent'=>'int'], -'SolrQuery::setGroupFacet' => ['SolrQuery', 'value'=>'bool'], -'SolrQuery::setGroupFormat' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::setGroupLimit' => ['SolrQuery', 'value'=>'int'], -'SolrQuery::setGroupMain' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::setGroupNGroups' => ['SolrQuery', 'value'=>'bool'], -'SolrQuery::setGroupOffset' => ['SolrQuery', 'value'=>'int'], -'SolrQuery::setGroupTruncate' => ['SolrQuery', 'value'=>'bool'], -'SolrQuery::setHighlight' => ['SolrQuery', 'flag'=>'bool'], 'SolrQuery::setHighlightAlternateField' => ['SolrQuery', 'field'=>'string', 'field_override='=>'string'], 'SolrQuery::setHighlightFormatter' => ['SolrQuery', 'formatter'=>'string', 'field_override='=>'string'], 'SolrQuery::setHighlightFragmenter' => ['SolrQuery', 'fragmenter'=>'string', 'field_override='=>'string'], 'SolrQuery::setHighlightFragsize' => ['SolrQuery', 'size'=>'int', 'field_override='=>'string'], -'SolrQuery::setHighlightHighlightMultiTerm' => ['SolrQuery', 'flag'=>'bool'], 'SolrQuery::setHighlightMaxAlternateFieldLength' => ['SolrQuery', 'fieldlength'=>'int', 'field_override='=>'string'], -'SolrQuery::setHighlightMaxAnalyzedChars' => ['SolrQuery', 'value'=>'int'], 'SolrQuery::setHighlightMergeContiguous' => ['SolrQuery', 'flag'=>'bool', 'field_override='=>'string'], -'SolrQuery::setHighlightRegexMaxAnalyzedChars' => ['SolrQuery', 'maxanalyzedchars'=>'int'], -'SolrQuery::setHighlightRegexPattern' => ['SolrQuery', 'value'=>'string'], -'SolrQuery::setHighlightRegexSlop' => ['SolrQuery', 'factor'=>'float'], -'SolrQuery::setHighlightRequireFieldMatch' => ['SolrQuery', 'flag'=>'bool'], 'SolrQuery::setHighlightSimplePost' => ['SolrQuery', 'simplepost'=>'string', 'field_override='=>'string'], 'SolrQuery::setHighlightSimplePre' => ['SolrQuery', 'simplepre'=>'string', 'field_override='=>'string'], 'SolrQuery::setHighlightSnippets' => ['SolrQuery', 'value'=>'int', 'field_override='=>'string'], -'SolrQuery::setHighlightUsePhraseHighlighter' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setMlt' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setMltBoost' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setMltCount' => ['SolrQuery', 'count'=>'int'], -'SolrQuery::setMltMaxNumQueryTerms' => ['SolrQuery', 'value'=>'int'], -'SolrQuery::setMltMaxNumTokens' => ['SolrQuery', 'value'=>'int'], -'SolrQuery::setMltMaxWordLength' => ['SolrQuery', 'maxwordlength'=>'int'], -'SolrQuery::setMltMinDocFrequency' => ['SolrQuery', 'mindocfrequency'=>'int'], -'SolrQuery::setMltMinTermFrequency' => ['SolrQuery', 'mintermfrequency'=>'int'], -'SolrQuery::setMltMinWordLength' => ['SolrQuery', 'minwordlength'=>'int'], -'SolrQuery::setOmitHeader' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setParam' => ['SolrParams', 'name'=>'string', 'value'=>''], -'SolrQuery::setQuery' => ['SolrQuery', 'query'=>'string'], -'SolrQuery::setRows' => ['SolrQuery', 'rows'=>'int'], -'SolrQuery::setShowDebugInfo' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setStart' => ['SolrQuery', 'start'=>'int'], -'SolrQuery::setStats' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setTerms' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setTermsField' => ['SolrQuery', 'fieldname'=>'string'], -'SolrQuery::setTermsIncludeLowerBound' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setTermsIncludeUpperBound' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setTermsLimit' => ['SolrQuery', 'limit'=>'int'], -'SolrQuery::setTermsLowerBound' => ['SolrQuery', 'lowerbound'=>'string'], -'SolrQuery::setTermsMaxCount' => ['SolrQuery', 'frequency'=>'int'], -'SolrQuery::setTermsMinCount' => ['SolrQuery', 'frequency'=>'int'], -'SolrQuery::setTermsPrefix' => ['SolrQuery', 'prefix'=>'string'], -'SolrQuery::setTermsReturnRaw' => ['SolrQuery', 'flag'=>'bool'], -'SolrQuery::setTermsSort' => ['SolrQuery', 'sorttype'=>'int'], -'SolrQuery::setTermsUpperBound' => ['SolrQuery', 'upperbound'=>'string'], -'SolrQuery::setTimeAllowed' => ['SolrQuery', 'timeallowed'=>'int'], -'SolrQuery::toString' => ['string', 'url_encode='=>'bool|false'], 'SolrQuery::unserialize' => ['void', 'serialized'=>'string'], -'SolrQueryResponse::__construct' => ['void'], -'SolrQueryResponse::__destruct' => [''], -'SolrQueryResponse::getDigestedResponse' => ['string'], -'SolrQueryResponse::getHttpStatus' => ['int'], -'SolrQueryResponse::getHttpStatusMessage' => ['string'], -'SolrQueryResponse::getRawRequest' => ['string'], -'SolrQueryResponse::getRawRequestHeaders' => ['string'], -'SolrQueryResponse::getRawResponse' => ['string'], -'SolrQueryResponse::getRawResponseHeaders' => ['string'], -'SolrQueryResponse::getRequestUrl' => ['string'], -'SolrQueryResponse::getResponse' => ['SolrObject'], -'SolrQueryResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], -'SolrQueryResponse::success' => ['bool'], -'SolrResponse::getDigestedResponse' => ['string'], -'SolrResponse::getHttpStatus' => ['int'], -'SolrResponse::getHttpStatusMessage' => ['string'], -'SolrResponse::getRawRequest' => ['string'], -'SolrResponse::getRawRequestHeaders' => ['string'], -'SolrResponse::getRawResponse' => ['string'], -'SolrResponse::getRawResponseHeaders' => ['string'], -'SolrResponse::getRequestUrl' => ['string'], -'SolrResponse::getResponse' => ['SolrObject'], -'SolrResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], -'SolrResponse::success' => ['bool'], -'SolrServerException::__clone' => ['void'], -'SolrServerException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], -'SolrServerException::__toString' => ['string'], -'SolrServerException::__wakeup' => ['void'], -'SolrServerException::getCode' => ['int'], -'SolrServerException::getFile' => ['string'], -'SolrServerException::getInternalInfo' => ['array'], -'SolrServerException::getLine' => ['int'], -'SolrServerException::getMessage' => ['string'], -'SolrServerException::getPrevious' => ['Exception|Throwable'], -'SolrServerException::getTrace' => ['array'], -'SolrServerException::getTraceAsString' => ['string'], -'SolrUpdateResponse::__construct' => ['void'], -'SolrUpdateResponse::__destruct' => [''], -'SolrUpdateResponse::getDigestedResponse' => ['string'], -'SolrUpdateResponse::getHttpStatus' => ['int'], -'SolrUpdateResponse::getHttpStatusMessage' => ['string'], -'SolrUpdateResponse::getRawRequest' => ['string'], -'SolrUpdateResponse::getRawRequestHeaders' => ['string'], -'SolrUpdateResponse::getRawResponse' => ['string'], -'SolrUpdateResponse::getRawResponseHeaders' => ['string'], -'SolrUpdateResponse::getRequestUrl' => ['string'], -'SolrUpdateResponse::getResponse' => ['SolrObject'], -'SolrUpdateResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], -'SolrUpdateResponse::success' => ['bool'], 'SolrUtils::digestXmlResponse' => ['SolrObject', 'xmlresponse'=>'string', 'parse_mode='=>'int'], 'SolrUtils::escapeQueryChars' => ['string|false', 'str'=>'string'], 'SolrUtils::getSolrVersion' => ['string'], @@ -11223,7 +9859,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'], @@ -11256,24 +9892,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'], @@ -11290,22 +9926,22 @@ 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], // Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues -'SplFileObject::fgetcsv' => ['array|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'SplFileObject::fgets' => ['string|false'], +'SplFileObject::fgetcsv' => ['non-empty-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'], @@ -11350,7 +9986,7 @@ 'spliti' => ['array', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], 'SplMaxHeap::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], 'SplMinHeap::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], -'SplObjectStorage::addAll' => ['void', 'os'=>'splobjectstorage'], +'SplObjectStorage::addAll' => ['0|positive-int', 'os'=>'SplObjectStorage'], 'SplObjectStorage::attach' => ['void', 'obj'=>'object', 'inf='=>'mixed'], 'SplObjectStorage::contains' => ['bool', 'obj'=>'object'], 'SplObjectStorage::count' => ['0|positive-int'], @@ -11364,14 +10000,14 @@ 'SplObjectStorage::offsetGet' => ['mixed', 'obj'=>'object'], 'SplObjectStorage::offsetSet' => ['object', 'object'=>'object', 'data='=>'mixed'], 'SplObjectStorage::offsetUnset' => ['object', 'object'=>'object'], -'SplObjectStorage::removeAll' => ['void', 'os'=>'splobjectstorage'], -'SplObjectStorage::removeAllExcept' => ['void', 'os'=>'splobjectstorage'], +'SplObjectStorage::removeAll' => ['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' => ['0|positive-int'], 'SplPriorityQueue::current' => ['mixed'], @@ -11390,8 +10026,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'], @@ -11401,7 +10037,7 @@ 'Spoofchecker::setAllowedLocales' => ['void', 'locale_list'=>'string'], 'Spoofchecker::setChecks' => ['void', 'checks'=>'long'], 'Spoofchecker::setRestrictionLevel' => ['void', 'restriction_level'=>'int'], -'sprintf' => ['string', 'format'=>'string', '...values='=>'string|int|float|bool'], +'sprintf' => ['string', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'sql_regcase' => ['string', 'string'=>'string'], 'SQLite3::__construct' => ['void', 'filename'=>'string', 'flags='=>'int', 'encryption_key='=>'string|null'], 'SQLite3::busyTimeout' => ['bool', 'msecs'=>'int'], @@ -11418,7 +10054,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'], @@ -11540,7 +10176,7 @@ 'sqlsrv_prepare' => ['resource|false', 'conn'=>'resource', 'sql'=>'string', 'params='=>'array', 'options='=>'array'], 'sqlsrv_query' => ['resource|false', 'conn'=>'resource', 'sql'=>'string', 'params='=>'array', 'options='=>'array'], 'sqlsrv_rollback' => ['bool', 'conn'=>'resource'], -'sqlsrv_rows_affected' => ['int|false', 'stmt'=>'resource'], +'sqlsrv_rows_affected' => ['int<-1,max>|false', 'stmt'=>'resource'], 'sqlsrv_send_stream_data' => ['bool', 'stmt'=>'resource'], 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'number'=>'float'], @@ -11689,57 +10325,57 @@ 'stomp_version' => ['string'], 'StompException::getDetails' => ['string'], 'StompFrame::__construct' => ['void', 'command='=>'string', 'headers='=>'array', 'body='=>'string'], -'str_getcsv' => ['array', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'str_getcsv' => ['non-empty-list', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_pad' => ['string', 'input'=>'string', 'pad_length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'input'=>'string', 'multiplier'=>'int'], 'str_replace' => ['string|array', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_rot13' => ['string', 'str'=>'string'], 'str_shuffle' => ['string', 'str'=>'string'], -'str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'int'], +'str_split' => ['non-empty-list|false', 'str'=>'string', 'split_length='=>'positive-int'], 'str_word_count' => ['array|int|false', 'string'=>'string', 'format='=>'int', 'charlist='=>'string'], -'strcasecmp' => ['int', 'str1'=>'string', 'str2'=>'string'], +'strcasecmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strcmp' => ['int', 'str1'=>'string', 'str2'=>'string'], -'strcoll' => ['int', 'str1'=>'string', 'str2'=>'string'], -'strcspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], +'strcmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], +'strcoll' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], +'strcspn' => ['non-negative-int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], -'stream_bucket_make_writeable' => ['object|null', 'brigade'=>'resource'], -'stream_bucket_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'], 'stream_context_set_params' => ['bool', 'context'=>'resource', 'options'=>'array'], 'stream_copy_to_stream' => ['int|false', 'source'=>'resource', 'dest'=>'resource', 'maxlen='=>'int', 'pos='=>'int'], 'stream_encoding' => ['bool', 'stream'=>'resource', 'encoding='=>'string'], -'stream_filter_append' => ['resource|false', 'stream'=>'resource', 'filtername'=>'string', 'read_write='=>'int', 'filterparams='=>'array'], -'stream_filter_prepend' => ['resource|false', 'stream'=>'resource', 'filtername'=>'string', 'read_write='=>'int', 'filterparams='=>'array'], +'stream_filter_append' => ['resource|false', 'stream'=>'resource', 'filtername'=>'string', 'read_write='=>'int', 'params='=>'mixed'], +'stream_filter_prepend' => ['resource|false', 'stream'=>'resource', 'filtername'=>'string', 'read_write='=>'int', 'params='=>'mixed'], '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_contents' => ['__benevolent', 'source'=>'resource', 'maxlen='=>'int', 'offset='=>'int'], +'stream_get_filters' => ['list'], 'stream_get_line' => ['string|false', 'stream'=>'resource', 'maxlen'=>'int', 'ending='=>'string'], -'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], -'stream_get_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,crypto?:array{protocol:string,cipher_name:string,cipher_bits:int,cipher_version:string,alpn_protocol?:string}}', '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_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int-mask', 'context='=>'resource'], +'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'crypto_method='=>'STREAM_CRYPTO_METHOD_SSLv2_CLIENT|STREAM_CRYPTO_METHOD_SSLv3_CLIENT|STREAM_CRYPTO_METHOD_SSLv23_CLIENT|STREAM_CRYPTO_METHOD_ANY_CLIENT|STREAM_CRYPTO_METHOD_TLS_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT|STREAM_CRYPTO_METHOD_SSLv2_SERVER|STREAM_CRYPTO_METHOD_SSLv3_SERVER|STREAM_CRYPTO_METHOD_SSLv23_SERVER|STREAM_CRYPTO_METHOD_ANY_SERVER|STREAM_CRYPTO_METHOD_TLS_SERVER|STREAM_CRYPTO_METHOD_TLSv1_0_SERVER|STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER|STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'session_stream='=>'resource'], 'stream_socket_get_name' => ['string|false', 'stream'=>'resource', 'want_peer'=>'bool'], 'stream_socket_pair' => ['resource[]|false', 'domain'=>'int', 'type'=>'int', 'protocol'=>'int'], 'stream_socket_recvfrom' => ['string|false', 'stream'=>'resource', 'amount'=>'int', 'flags='=>'int', '&w_remote_addr='=>'string'], @@ -11782,10 +10418,10 @@ 'stripslashes' => ['string', 'str'=>'string'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], 'strlen' => ['0|positive-int', 'string'=>'string'], -'strnatcasecmp' => ['int', 's1'=>'string', 's2'=>'string'], -'strnatcmp' => ['int', 's1'=>'string', 's2'=>'string'], -'strncasecmp' => ['int', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], -'strncmp' => ['int', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], +'strnatcasecmp' => ['int<-1, 1>', 's1'=>'string', 's2'=>'string'], +'strnatcmp' => ['int<-1, 1>', 's1'=>'string', 's2'=>'string'], +'strncasecmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], +'strncmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], 'strpbrk' => ['string|false', 'haystack'=>'string', 'char_list'=>'string'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strptime' => ['array|false', 'datestr'=>'string', 'format'=>'string'], @@ -11793,18 +10429,18 @@ 'strrev' => ['string', 'str'=>'string'], 'strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], -'strspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], +'strspn' => ['non-negative-int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'str'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'token'=>'string'], -'strtolower' => ['string', 'str'=>'string'], +'strtok' => ['non-empty-string|false', 'str'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], +'strtolower' => ['lowercase-string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], -'strtoupper' => ['string', 'str'=>'string'], +'strtoupper' => ['uppercase-string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], -'strval' => ['string', 'var'=>'mixed'], -'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], -'substr_compare' => ['int|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], +'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'], @@ -12185,14 +10821,14 @@ 'SyncSharedMemory::size' => ['bool'], 'SyncSharedMemory::write' => ['', 'string='=>'string', 'start='=>'int'], 'sys_get_temp_dir' => ['string'], -'sys_getloadavg' => ['array|false'], +'sys_getloadavg' => ['array{float,float,float}|false'], 'syslog' => ['bool', 'priority'=>'int', 'message'=>'string'], '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'], @@ -12243,7 +10879,7 @@ 'Throwable::getLine' => ['int'], 'Throwable::getMessage' => ['string'], 'Throwable::getPrevious' => ['Throwable|null'], -'Throwable::getTrace' => ['array'], +'Throwable::getTrace' => ['list\',args?:list,object?:object}>'], 'Throwable::getTraceAsString' => ['string'], 'tidy::__construct' => ['void', 'filename='=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], 'tidy::body' => ['tidyNode'], @@ -12304,21 +10940,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|false', 'timezone'=>'string'], -'timezone_transitions_get' => ['array|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'timezone_transitions_get' => ['list|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'timezone_version_get' => ['string'], -'tmpfile' => ['resource|false'], -'token_get_all' => ['array', 'source'=>'string', 'flags='=>'int'], -'token_name' => ['string', 'type'=>'int'], +'tmpfile' => ['__benevolent'], +'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], +'token_name' => ['non-falsy-string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], 'TokyoTyrant::add' => ['int|float', 'key'=>'string', 'increment'=>'float', 'type='=>'int'], 'TokyoTyrant::connect' => ['TokyoTyrant', 'host'=>'string', 'port='=>'int', 'options='=>'array'], @@ -12375,183 +11011,183 @@ 'TokyoTyrantTable::putShl' => ['void', 'key'=>'string', 'value'=>'string', 'width'=>'int'], 'TokyoTyrantTable::setIndex' => ['mixed', 'column'=>'string', 'type'=>'int'], 'touch' => ['bool', 'filename'=>'string', 'time='=>'int', 'atime='=>'int'], -'trader_acos' => ['array', 'real'=>'array'], -'trader_ad' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array'], -'trader_add' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_adosc' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int'], -'trader_adx' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_adxr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_apo' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], -'trader_aroon' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_aroonosc' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_asin' => ['array', 'real'=>'array'], -'trader_atan' => ['array', 'real'=>'array'], -'trader_atr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_avgprice' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_bbands' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDevUp='=>'float', 'nbDevDn='=>'float', 'mAType='=>'int'], -'trader_beta' => ['array', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], -'trader_bop' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cci' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_cdl2crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3blackcrows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3inside' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3linestrike' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3outside' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3starsinsouth' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3whitesoldiers' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlabandonedbaby' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdladvanceblock' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlbelthold' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlbreakaway' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlclosingmarubozu' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlconcealbabyswall' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlcounterattack' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldarkcloudcover' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdldoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldragonflydoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlengulfing' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdleveningdojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdleveningstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlgapsidesidewhite' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlgravestonedoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhammer' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhangingman' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlharami' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlharamicross' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhighwave' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhikkake' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhikkakemod' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhomingpigeon' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlidentical3crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlinneck' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlinvertedhammer' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlkicking' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlkickingbylength' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlladderbottom' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdllongleggeddoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdllongline' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmarubozu' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmatchinglow' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmathold' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlmorningdojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlmorningstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlonneck' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlpiercing' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlrickshawman' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlrisefall3methods' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlseparatinglines' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlshootingstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlshortline' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlspinningtop' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlstalledpattern' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlsticksandwich' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltakuri' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltasukigap' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlthrusting' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltristar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlunique3river' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlupsidegap2crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlxsidegap3methods' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_ceil' => ['array', 'real'=>'array'], -'trader_cmo' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_correl' => ['array', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], -'trader_cos' => ['array', 'real'=>'array'], -'trader_cosh' => ['array', 'real'=>'array'], -'trader_dema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_div' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_dx' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_ema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], +'trader_acos' => ['array|false', 'real'=>'array'], +'trader_ad' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array'], +'trader_add' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_adosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int'], +'trader_adx' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_adxr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_apo' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], +'trader_aroon' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_aroonosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_asin' => ['array|false', 'real'=>'array'], +'trader_atan' => ['array|false', 'real'=>'array'], +'trader_atr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_avgprice' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_bbands' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDevUp='=>'float', 'nbDevDn='=>'float', 'mAType='=>'int'], +'trader_beta' => ['array|false', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], +'trader_bop' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cci' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_cdl2crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3blackcrows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3inside' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3linestrike' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3outside' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3starsinsouth' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3whitesoldiers' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlabandonedbaby' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdladvanceblock' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlbelthold' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlbreakaway' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlclosingmarubozu' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlconcealbabyswall' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlcounterattack' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldarkcloudcover' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdldoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldragonflydoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlengulfing' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdleveningdojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdleveningstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlgapsidesidewhite' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlgravestonedoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhammer' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhangingman' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlharami' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlharamicross' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhighwave' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhikkake' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhikkakemod' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhomingpigeon' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlidentical3crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlinneck' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlinvertedhammer' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlkicking' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlkickingbylength' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlladderbottom' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdllongleggeddoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdllongline' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmarubozu' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmatchinglow' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmathold' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlmorningdojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlmorningstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlonneck' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlpiercing' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlrickshawman' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlrisefall3methods' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlseparatinglines' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlshootingstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlshortline' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlspinningtop' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlstalledpattern' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlsticksandwich' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltakuri' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltasukigap' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlthrusting' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltristar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlunique3river' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlupsidegap2crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlxsidegap3methods' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_ceil' => ['array|false', 'real'=>'array'], +'trader_cmo' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_correl' => ['array|false', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], +'trader_cos' => ['array|false', 'real'=>'array'], +'trader_cosh' => ['array|false', 'real'=>'array'], +'trader_dema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_div' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_dx' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_ema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], 'trader_errno' => ['int'], -'trader_exp' => ['array', 'real'=>'array'], -'trader_floor' => ['array', 'real'=>'array'], +'trader_exp' => ['array|false', 'real'=>'array'], +'trader_floor' => ['array|false', 'real'=>'array'], 'trader_get_compat' => ['int'], 'trader_get_unstable_period' => ['int', 'functionId'=>'int'], -'trader_ht_dcperiod' => ['array', 'real'=>'array'], -'trader_ht_dcphase' => ['array', 'real'=>'array'], -'trader_ht_phasor' => ['array', 'real'=>'array'], -'trader_ht_sine' => ['array', 'real'=>'array'], -'trader_ht_trendline' => ['array', 'real'=>'array'], -'trader_ht_trendmode' => ['array', 'real'=>'array'], -'trader_kama' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_angle' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_intercept' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_slope' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_ln' => ['array', 'real'=>'array'], -'trader_log10' => ['array', 'real'=>'array'], -'trader_ma' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'mAType='=>'int'], -'trader_macd' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'signalPeriod='=>'int'], -'trader_macdext' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'fastMAType='=>'int', 'slowPeriod='=>'int', 'slowMAType='=>'int', 'signalPeriod='=>'int', 'signalMAType='=>'int'], -'trader_macdfix' => ['array', 'real'=>'array', 'signalPeriod='=>'int'], -'trader_mama' => ['array', 'real'=>'array', 'fastLimit='=>'float', 'slowLimit='=>'float'], -'trader_mavp' => ['array', 'real'=>'array', 'periods'=>'array', 'minPeriod='=>'int', 'maxPeriod='=>'int', 'mAType='=>'int'], -'trader_max' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_maxindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_medprice' => ['array', 'high'=>'array', 'low'=>'array'], -'trader_mfi' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'timePeriod='=>'int'], -'trader_midpoint' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_midprice' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_min' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minmax' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minmaxindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minus_di' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_minus_dm' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_mom' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_mult' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_natr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_obv' => ['array', 'real'=>'array', 'volume'=>'array'], -'trader_plus_di' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_plus_dm' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_ppo' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], -'trader_roc' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocp' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocr' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocr100' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rsi' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_sar' => ['array', 'high'=>'array', 'low'=>'array', 'acceleration='=>'float', 'maximum='=>'float'], -'trader_sarext' => ['array', 'high'=>'array', 'low'=>'array', 'startValue='=>'float', 'offsetOnReverse='=>'float', 'accelerationInitLong='=>'float', 'accelerationLong='=>'float', 'accelerationMaxLong='=>'float', 'accelerationInitShort='=>'float', 'accelerationShort='=>'float', 'accelerationMaxShort='=>'float'], +'trader_ht_dcperiod' => ['array|false', 'real'=>'array'], +'trader_ht_dcphase' => ['array|false', 'real'=>'array'], +'trader_ht_phasor' => ['array|false', 'real'=>'array'], +'trader_ht_sine' => ['array|false', 'real'=>'array'], +'trader_ht_trendline' => ['array|false', 'real'=>'array'], +'trader_ht_trendmode' => ['array|false', 'real'=>'array'], +'trader_kama' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_angle' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_intercept' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_slope' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_ln' => ['array|false', 'real'=>'array'], +'trader_log10' => ['array|false', 'real'=>'array'], +'trader_ma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'mAType='=>'int'], +'trader_macd' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'signalPeriod='=>'int'], +'trader_macdext' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'fastMAType='=>'int', 'slowPeriod='=>'int', 'slowMAType='=>'int', 'signalPeriod='=>'int', 'signalMAType='=>'int'], +'trader_macdfix' => ['array|false', 'real'=>'array', 'signalPeriod='=>'int'], +'trader_mama' => ['array|false', 'real'=>'array', 'fastLimit='=>'float', 'slowLimit='=>'float'], +'trader_mavp' => ['array|false', 'real'=>'array', 'periods'=>'array', 'minPeriod='=>'int', 'maxPeriod='=>'int', 'mAType='=>'int'], +'trader_max' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_maxindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_medprice' => ['array|false', 'high'=>'array', 'low'=>'array'], +'trader_mfi' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'timePeriod='=>'int'], +'trader_midpoint' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_midprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_min' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minmax' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minmaxindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minus_di' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_minus_dm' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_mom' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_mult' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_natr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_obv' => ['array|false', 'real'=>'array', 'volume'=>'array'], +'trader_plus_di' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_plus_dm' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_ppo' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], +'trader_roc' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocp' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocr' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocr100' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rsi' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sar' => ['array|false', 'high'=>'array', 'low'=>'array', 'acceleration='=>'float', 'maximum='=>'float'], +'trader_sarext' => ['array|false', 'high'=>'array', 'low'=>'array', 'startValue='=>'float', 'offsetOnReverse='=>'float', 'accelerationInitLong='=>'float', 'accelerationLong='=>'float', 'accelerationMaxLong='=>'float', 'accelerationInitShort='=>'float', 'accelerationShort='=>'float', 'accelerationMaxShort='=>'float'], 'trader_set_compat' => ['void', 'compatId'=>'int'], 'trader_set_unstable_period' => ['void', 'functionId'=>'int', 'timePeriod'=>'int'], -'trader_sin' => ['array', 'real'=>'array'], -'trader_sinh' => ['array', 'real'=>'array'], -'trader_sma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_sqrt' => ['array', 'real'=>'array'], -'trader_stddev' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], -'trader_stoch' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'slowK_Period='=>'int', 'slowK_MAType='=>'int', 'slowD_Period='=>'int', 'slowD_MAType='=>'int'], -'trader_stochf' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], -'trader_stochrsi' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], -'trader_sub' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_sum' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_t3' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'vFactor='=>'float'], -'trader_tan' => ['array', 'real'=>'array'], -'trader_tanh' => ['array', 'real'=>'array'], -'trader_tema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_trange' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_trima' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_trix' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_tsf' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_typprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_ultosc' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod1='=>'int', 'timePeriod2='=>'int', 'timePeriod3='=>'int'], -'trader_var' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], -'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sin' => ['array|false', 'real'=>'array'], +'trader_sinh' => ['array|false', 'real'=>'array'], +'trader_sma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sqrt' => ['array|false', 'real'=>'array'], +'trader_stddev' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], +'trader_stoch' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'slowK_Period='=>'int', 'slowK_MAType='=>'int', 'slowD_Period='=>'int', 'slowD_MAType='=>'int'], +'trader_stochf' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], +'trader_stochrsi' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], +'trader_sub' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_sum' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_t3' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'vFactor='=>'float'], +'trader_tan' => ['array|false', 'real'=>'array'], +'trader_tanh' => ['array|false', 'real'=>'array'], +'trader_tema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_trange' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_trima' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_trix' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_tsf' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_typprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_ultosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod1='=>'int', 'timePeriod2='=>'int', 'timePeriod3='=>'int'], +'trader_var' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], +'trader_wclprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_willr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_wma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], 'trait_exists' => ['bool', 'traitname'=>'string', 'autoload='=>'bool'], 'Transliterator::create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'Transliterator::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'Transliterator::createInverse' => ['Transliterator'], -'Transliterator::getErrorCode' => ['int'], -'Transliterator::getErrorMessage' => ['string'], -'Transliterator::listIDs' => ['array'], +'Transliterator::createInverse' => ['?Transliterator'], +'Transliterator::getErrorCode' => ['int|false'], +'Transliterator::getErrorMessage' => ['string|false'], +'Transliterator::listIDs' => ['list|false'], 'Transliterator::transliterate' => ['string|false', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'transliterator_create_inverse' => ['Transliterator', 'obj'=>'Transliterator'], +'transliterator_create_inverse' => ['?Transliterator', 'obj'=>'Transliterator'], 'transliterator_get_error_code' => ['int|false', 'obj'=>'Transliterator'], 'transliterator_get_error_message' => ['string|false', 'obj'=>'Transliterator'], -'transliterator_list_ids' => ['array|false'], +'transliterator_list_ids' => ['list|false'], 'transliterator_transliterate' => ['string|false', 'obj'=>'Transliterator|string', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'trigger_error' => ['bool', 'message'=>'string', 'error_type='=>'int'], 'trim' => ['string', 'str'=>'string', 'character_mask='=>'string'], @@ -12563,7 +11199,7 @@ 'TypeError::getLine' => ['int'], 'TypeError::getMessage' => ['string'], 'TypeError::getPrevious' => ['Throwable|TypeError|null'], -'TypeError::getTrace' => ['array'], +'TypeError::getTrace' => ['list\',args?:list,object?:object}>'], 'TypeError::getTraceAsString' => ['string'], 'uasort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'ucfirst' => ['string', 'str'=>'string'], @@ -12624,7 +11260,7 @@ 'UnderflowException::getLine' => ['int'], 'UnderflowException::getMessage' => ['string'], 'UnderflowException::getPrevious' => ['Throwable|UnderflowException|null'], -'UnderflowException::getTrace' => ['array'], +'UnderflowException::getTrace' => ['list\',args?:list,object?:object}>'], 'UnderflowException::getTraceAsString' => ['string'], 'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnexpectedValueException)'], @@ -12634,18 +11270,17 @@ 'UnexpectedValueException::getLine' => ['int'], 'UnexpectedValueException::getMessage' => ['string'], 'UnexpectedValueException::getPrevious' => ['Throwable|UnexpectedValueException|null'], -'UnexpectedValueException::getTrace' => ['array'], +'UnexpectedValueException::getTrace' => ['list\',args?:list,object?:object}>'], 'UnexpectedValueException::getTraceAsString' => ['string'], -'uniqid' => ['string', 'prefix='=>'string', 'more_entropy='=>'bool'], +'uniqid' => ['non-empty-string', 'prefix='=>'string', 'more_entropy='=>'bool'], 'unixtojd' => ['int|false', 'timestamp='=>'int'], 'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'], 'unpack' => ['array|false', 'format'=>'string', 'data'=>'string', 'offset='=>'int'], 'unregister_tick_function' => ['void', 'function_name'=>'callable'], 'unserialize' => ['mixed', 'variable_representation'=>'string', 'allowed_classes='=>'array{allowed_classes?:string[]|bool}'], -'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], 'uopz_add_function' => ['bool', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool', '$all'=>'bool'], -'uopz_add_function\1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], +'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'], @@ -12653,23 +11288,23 @@ 'uopz_copy' => ['Closure', 'class'=>'string', 'function'=>'string'], 'uopz_copy\'1' => ['Closure', 'function'=>'string'], 'uopz_del_function' => ['bool', 'class'=>'string', 'function'=>'string', '$all'=>'bool'], -'uopz_del_function\1' => ['bool', 'function'=>'string'], +'uopz_del_function\'1' => ['bool', 'function'=>'string'], 'uopz_delete' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_delete\'1' => ['void', 'function'=>'string'], 'uopz_extend' => ['void', 'class'=>'string', 'parent'=>'string'], -'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags'=>'int'], -'uopz_flags\'1' => ['int', 'function'=>'string', 'flags'=>'int'], +'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags='=>'int'], +'uopz_flags\'1' => ['int', 'function'=>'string', 'flags='=>'int'], 'uopz_function' => ['void', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_function\'1' => ['void', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_get_exit_status' => ['mixed'], 'uopz_get_hook' => ['Closure', 'class'=>'string', 'function'=>'string'], -'uopz_get_hook\1' => ['Closure', 'function'=>'string'], +'uopz_get_hook\'1' => ['Closure', 'function'=>'string'], 'uopz_get_mock' => ['mixed', 'class'=>'string'], -'uopz_get_property' => ['void', 'class'=>'string', 'property'=>'string'], -'uopz_get_property\1' => ['void', 'instance'=>'object', 'property'=>'string'], +'uopz_get_property' => ['mixed', 'class'=>'string', 'property'=>'string'], +'uopz_get_property\'1' => ['mixed', 'instance'=>'object', 'property'=>'string'], 'uopz_get_return' => ['mixed', 'class='=>'string', 'function='=>'string'], 'uopz_get_static' => ['array', 'class='=>'string', 'function='=>'string'], -'uopz_get_static\1' => ['array', 'function='=>'string'], +'uopz_get_static\'1' => ['array', 'function='=>'string'], 'uopz_implement' => ['void', 'class'=>'string', 'interface'=>'string'], 'uopz_overload' => ['void', 'opcode'=>'int', 'callable'=>'Callable'], 'uopz_redefine' => ['void', 'class'=>'string', 'constant'=>'string', 'value'=>'mixed'], @@ -12680,13 +11315,13 @@ 'uopz_restore\'1' => ['void', 'function'=>'string'], 'uopz_set_mock' => ['void', 'class'=>'string', 'mock'=>'object|string'], 'uopz_set_property' => ['void', 'class'=>'string', 'property'=>'string', 'value'=>'mixed'], -'uopz_set_property\1' => ['void', 'instance'=>'object', 'property'=>'string', 'value'=>'mixed'], +'uopz_set_property\'1' => ['void', 'instance'=>'object', 'property'=>'string', 'value'=>'mixed'], 'uopz_set_return' => ['bool', 'class'=>'string', 'function'=>'string', 'value'=>'mixed', 'execute='=>'bool'], 'uopz_set_return\'1' => ['bool', 'function'=>'string', 'value'=>'mixed', 'execute='=>'bool'], 'uopz_undefine' => ['void', 'class'=>'string', 'constant'=>'string'], 'uopz_undefine\'1' => ['void', 'constant'=>'string'], 'uopz_set_hook' => ['bool', 'class'=>'string', 'function'=>'string', 'hook'=>'Closure'], -'uopz_set_hook\1' => ['bool', 'function'=>'string', 'hook'=>'Closure'], +'uopz_set_hook\'1' => ['bool', 'function'=>'string', 'hook'=>'Closure'], 'uopz_unset_mock' => ['void', 'class'=>'string'], 'uopz_unset_return' => ['bool', 'class='=>'string', 'function='=>'string'], 'uopz_unset_return\'1' => ['bool', 'function'=>'string'], @@ -12730,7 +11365,7 @@ 'V8JsScriptException::getLine' => ['int'], 'V8JsScriptException::getMessage' => ['string'], 'V8JsScriptException::getPrevious' => ['Exception|Throwable'], -'V8JsScriptException::getTrace' => ['array'], +'V8JsScriptException::getTrace' => ['list\',args?:list,object?:object}>'], 'V8JsScriptException::getTraceAsString' => ['string'], 'var_dump' => ['void', 'var'=>'mixed', '...args='=>'mixed'], 'var_export' => ['string|null', 'var'=>'mixed', 'return='=>'bool'], @@ -12786,8 +11421,8 @@ 'VarnishStat::__construct' => ['void', 'args='=>'array'], 'VarnishStat::getSnapshot' => ['array'], 'version_compare' => ['int', 'version1'=>'string', 'version2'=>'string'], -'version_compare\'1' => ['bool', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string'], -'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array'], +'version_compare\'1' => ['__benevolent', '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'], @@ -12824,8 +11459,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='=>''], @@ -12967,12 +11602,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'], @@ -12984,7 +11620,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'], @@ -12993,9 +11629,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[]'], @@ -13026,7 +11663,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'], @@ -13040,16 +11677,16 @@ 'xml_parser_free' => ['bool', 'parser'=>'resource'], 'xml_parser_get_option' => ['mixed', 'parser'=>'resource', 'option'=>'int'], 'xml_parser_set_option' => ['bool', 'parser'=>'resource', 'option'=>'int', 'value'=>'mixed'], -'xml_set_character_data_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_default_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_element_handler' => ['bool', 'parser'=>'resource', 'shdl'=>'callable', 'ehdl'=>'callable'], -'xml_set_end_namespace_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_external_entity_ref_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_notation_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], +'xml_set_character_data_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_default_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_element_handler' => ['bool', 'parser'=>'resource', 'shdl'=>'callable|string|null', 'ehdl'=>'callable|string|null'], +'xml_set_end_namespace_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_external_entity_ref_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_notation_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], 'xml_set_object' => ['bool', 'parser'=>'resource', 'obj'=>'object'], -'xml_set_processing_instruction_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_start_namespace_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], -'xml_set_unparsed_entity_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable'], +'xml_set_processing_instruction_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_start_namespace_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], +'xml_set_unparsed_entity_decl_handler' => ['bool', 'parser'=>'resource', 'hdl'=>'callable|string|null'], 'XMLDiff\Base::__construct' => ['void', 'nsname'=>'string'], 'XMLDiff\Base::diff' => ['mixed', 'from'=>'mixed', 'to'=>'mixed'], 'XMLDiff\Base::merge' => ['mixed', 'src'=>'mixed', 'diff'=>'mixed'], @@ -13083,7 +11720,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'], @@ -13125,7 +11762,7 @@ 'XMLWriter::startDTDElement' => ['bool', 'qualifiedname'=>'string'], 'XMLWriter::startDTDEntity' => ['bool', 'name'=>'string', 'isparam'=>'bool'], 'XMLWriter::startElement' => ['bool', 'name'=>'string'], -'XMLWriter::startElementNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], +'XMLWriter::startElementNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string|null'], 'XMLWriter::startPI' => ['bool', 'target'=>'string'], 'XMLWriter::text' => ['bool', 'content'=>'string'], 'XMLWriter::writeAttribute' => ['bool', 'name'=>'string', 'value'=>'string'], @@ -13167,7 +11804,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], -'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], +'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], @@ -13215,7 +11852,7 @@ 'xslt_set_scheme_handler' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_set_scheme_handlers' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_setopt' => ['', 'processor'=>'', 'newmask'=>'int'], -'XSLTProcessor::getParameter' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], +'XSLTProcessor::getParameter' => ['string|false', 'namespaceuri'=>'string', 'localname'=>'string'], 'XsltProcessor::getSecurityPrefs' => ['int'], 'XSLTProcessor::hasExsltSupport' => ['bool'], 'XSLTProcessor::importStylesheet' => ['bool', 'stylesheet'=>'object'], @@ -13225,9 +11862,9 @@ 'XSLTProcessor::setParameter\'1' => ['bool', 'namespace'=>'string', 'options'=>'array'], 'XSLTProcessor::setProfiling' => ['bool', 'filename'=>'string'], 'XsltProcessor::setSecurityPrefs' => ['int', 'securityPrefs'=>'int'], -'XSLTProcessor::transformToDoc' => ['DOMDocument', 'doc'=>'DOMNode'], +'XSLTProcessor::transformToDoc' => ['DOMDocument|false', 'doc'=>'DOMNode'], 'XSLTProcessor::transformToURI' => ['int', 'doc'=>'DOMDocument', 'uri'=>'string'], -'XSLTProcessor::transformToXML' => ['string|false', 'doc'=>'DOMDocument|SimpleXMLElement'], +'XSLTProcessor::transformToXML' => ['string|false|null', 'doc'=>'DOMDocument|SimpleXMLElement'], 'Yaconf::get' => ['mixed', 'name'=>'string', 'default_value='=>'mixed'], 'Yaconf::has' => ['bool', 'name'=>'string'], 'Yaf_Action_Abstract::__construct' => ['void', 'request'=>'Yaf_Request_Abstract', 'response'=>'Yaf_Response_Abstract', 'view'=>'Yaf_View_Interface', 'invokeArgs='=>'?array'], @@ -13250,7 +11887,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'], @@ -13265,93 +11902,113 @@ 'Yaf_Application::getModules' => ['array'], 'Yaf_Application::run' => ['void'], 'Yaf_Application::setAppDirectory' => ['Yaf_Application', 'directory'=>'string'], -'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::__get' => ['mixed', 'name'=>'string'], +'Yaf_Config_Abstract::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Abstract::count' => ['0|positive-int'], +'Yaf_Config_Abstract::current' => ['mixed'], +'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'?string'], +'Yaf_Config_Abstract::key' => ['int|string|null|bool'], +'Yaf_Config_Abstract::next' => ['void'], +'Yaf_Config_Abstract::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Abstract::offsetUnset' => ['void', 'name'=>'mixed'], 'Yaf_Config_Abstract::readonly' => ['bool'], -'Yaf_Config_Abstract::set' => ['Yaf_Config_Abstract'], +'Yaf_Config_Abstract::rewind' => ['void'], +'Yaf_Config_Abstract::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Abstract::toArray' => ['array'], -'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], -'Yaf_Config_Ini::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Ini::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::valid' => ['bool'], +'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'?string'], +'Yaf_Config_Ini::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Ini::__set' => ['void', 'name'=>'mixed', 'value'=>'mixed'], 'Yaf_Config_Ini::count' => ['0|positive-int'], -'Yaf_Config_Ini::current' => ['void'], -'Yaf_Config_Ini::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Ini::key' => ['void'], +'Yaf_Config_Ini::current' => ['mixed'], +'Yaf_Config_Ini::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Ini::key' => ['int|string|null|bool'], 'Yaf_Config_Ini::next' => ['void'], -'Yaf_Config_Ini::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::readonly' => ['void'], +'Yaf_Config_Ini::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Ini::readonly' => ['bool'], 'Yaf_Config_Ini::rewind' => ['void'], -'Yaf_Config_Ini::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Ini::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Ini::toArray' => ['array'], -'Yaf_Config_Ini::valid' => ['void'], -'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], +'Yaf_Config_Ini::valid' => ['bool'], +'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'string'], 'Yaf_Config_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Simple::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'string'], +'Yaf_Config_Simple::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::count' => ['0|positive-int'], -'Yaf_Config_Simple::current' => ['void'], -'Yaf_Config_Simple::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Simple::key' => ['void'], +'Yaf_Config_Simple::current' => ['mixed'], +'Yaf_Config_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Simple::key' => ['int|string|null|bool'], 'Yaf_Config_Simple::next' => ['void'], -'Yaf_Config_Simple::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::readonly' => ['void'], +'Yaf_Config_Simple::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Simple::readonly' => ['bool'], 'Yaf_Config_Simple::rewind' => ['void'], -'Yaf_Config_Simple::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Simple::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::toArray' => ['array'], -'Yaf_Config_Simple::valid' => ['void'], +'Yaf_Config_Simple::valid' => ['bool'], 'Yaf_Controller_Abstract::__clone' => ['void'], 'Yaf_Controller_Abstract::__construct' => ['void'], -'Yaf_Controller_Abstract::display' => ['bool', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward' => ['void', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'1' => ['void', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'2' => ['void', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::getInvokeArg' => ['void', 'name'=>'string'], -'Yaf_Controller_Abstract::getInvokeArgs' => ['void'], -'Yaf_Controller_Abstract::getModuleName' => ['string'], -'Yaf_Controller_Abstract::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Controller_Abstract::getResponse' => ['Yaf_Response_Abstract'], -'Yaf_Controller_Abstract::getView' => ['Yaf_View_Interface'], -'Yaf_Controller_Abstract::getViewpath' => ['void'], +'Yaf_Controller_Abstract::display' => ['?bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::forward' => ['?bool', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'1' => ['?bool', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'2' => ['?bool', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'3' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'4' => ['?bool', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'5' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::getInvokeArg' => ['?string', 'name'=>'string'], +'Yaf_Controller_Abstract::getInvokeArgs' => ['?array'], +'Yaf_Controller_Abstract::getModuleName' => ['?string'], +'Yaf_Controller_Abstract::getName' => ['?string'], +'Yaf_Controller_Abstract::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Controller_Abstract::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Controller_Abstract::getView' => ['?Yaf_View_Interface'], +'Yaf_Controller_Abstract::getViewpath' => ['?string'], 'Yaf_Controller_Abstract::init' => ['void'], -'Yaf_Controller_Abstract::initView' => ['void', 'options='=>'array'], -'Yaf_Controller_Abstract::redirect' => ['bool', 'url'=>'string'], -'Yaf_Controller_Abstract::render' => ['string', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::setViewpath' => ['void', 'view_directory'=>'string'], +'Yaf_Controller_Abstract::initView' => ['?Yaf_View_Interface', 'options='=>'?array'], +'Yaf_Controller_Abstract::redirect' => ['?bool', 'url'=>'string'], +'Yaf_Controller_Abstract::render' => ['string|null|bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::setViewpath' => ['?bool', 'view_directory'=>'string'], 'Yaf_Dispatcher::__clone' => ['void'], 'Yaf_Dispatcher::__construct' => ['void'], -'Yaf_Dispatcher::__sleep' => ['void'], +'Yaf_Dispatcher::__sleep' => ['list'], 'Yaf_Dispatcher::__wakeup' => ['void'], -'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::disableView' => ['bool'], -'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::enableView' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::getApplication' => ['Yaf_Application'], -'Yaf_Dispatcher::getInstance' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Dispatcher::getRouter' => ['Yaf_Router'], -'Yaf_Dispatcher::initView' => ['Yaf_View_Interface', 'templates_dir'=>'string', 'options='=>'array'], -'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher', 'plugin'=>'Yaf_Plugin_Abstract'], -'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher', 'flag'=>'bool'], -'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher', 'action'=>'string'], -'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher', 'controller'=>'string'], -'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher', 'module'=>'string'], -'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher', 'callback'=>'call', 'error_types'=>'int'], -'Yaf_Dispatcher::setRequest' => ['Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::setView' => ['Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], -'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher', 'flag='=>'bool'], +'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::disableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract|false|null', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::enableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::getApplication' => ['?Yaf_Application'], +'Yaf_Dispatcher::getDefaultAction' => ['?string'], +'Yaf_Dispatcher::getDefaultController' => ['?string'], +'Yaf_Dispatcher::getDefaultModule' => ['?string'], +'Yaf_Dispatcher::getInstance' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Dispatcher::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Dispatcher::getRouter' => ['?Yaf_Router'], +'Yaf_Dispatcher::initView' => ['Yaf_View_Interface|null|false', 'templates_dir'=>'string', 'options='=>'?array'], +'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher|false|null', 'plugin'=>'Yaf_Plugin_Abstract'], +'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher|false|null', 'flag='=>'bool'], +'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher|false|null', 'action'=>'string'], +'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher|false|null', 'controller'=>'string'], +'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher|false|null', 'module'=>'string'], +'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher|false|null', 'callback'=>'mixed', 'error_types'=>'int'], +'Yaf_Dispatcher::setRequest' => ['?Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::setResponse' => ['?Yaf_Dispatcher', 'response'=>'Yaf_Response_Abstract'], +'Yaf_Dispatcher::setView' => ['?Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], +'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], 'Yaf_Exception::__construct' => ['void'], 'Yaf_Exception::getPrevious' => ['void'], 'Yaf_Loader::__clone' => ['void'], 'Yaf_Loader::__construct' => ['void'], -'Yaf_Loader::__sleep' => ['void'], +'Yaf_Loader::__sleep' => ['list'], 'Yaf_Loader::__wakeup' => ['void'], 'Yaf_Loader::autoload' => ['void'], 'Yaf_Loader::clearLocalNamespace' => ['void'], @@ -13375,149 +12032,163 @@ 'Yaf_Registry::get' => ['mixed', 'name'=>'string'], 'Yaf_Registry::has' => ['bool', 'name'=>'string'], 'Yaf_Registry::set' => ['bool', 'name'=>'string', 'value'=>'string'], -'Yaf_Request_Abstract::getActionName' => ['void'], -'Yaf_Request_Abstract::getBaseUri' => ['void'], -'Yaf_Request_Abstract::getControllerName' => ['void'], -'Yaf_Request_Abstract::getEnv' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getException' => ['void'], -'Yaf_Request_Abstract::getLanguage' => ['void'], -'Yaf_Request_Abstract::getMethod' => ['void'], -'Yaf_Request_Abstract::getModuleName' => ['void'], -'Yaf_Request_Abstract::getParam' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getParams' => ['void'], -'Yaf_Request_Abstract::getRequestUri' => ['void'], -'Yaf_Request_Abstract::getServer' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::isCli' => ['void'], -'Yaf_Request_Abstract::isDispatched' => ['void'], -'Yaf_Request_Abstract::isGet' => ['void'], -'Yaf_Request_Abstract::isHead' => ['void'], -'Yaf_Request_Abstract::isOptions' => ['void'], -'Yaf_Request_Abstract::isPost' => ['void'], -'Yaf_Request_Abstract::isPut' => ['void'], -'Yaf_Request_Abstract::isRouted' => ['void'], -'Yaf_Request_Abstract::isXmlHttpRequest' => ['void'], -'Yaf_Request_Abstract::setActionName' => ['void', 'action'=>'string'], -'Yaf_Request_Abstract::setBaseUri' => ['bool', 'uir'=>'string'], -'Yaf_Request_Abstract::setControllerName' => ['void', 'controller'=>'string'], -'Yaf_Request_Abstract::setDispatched' => ['void'], -'Yaf_Request_Abstract::setModuleName' => ['void', 'module'=>'string'], -'Yaf_Request_Abstract::setParam' => ['void', 'name'=>'string', 'value='=>'string'], -'Yaf_Request_Abstract::setRequestUri' => ['void', 'uir'=>'string'], -'Yaf_Request_Abstract::setRouted' => ['void', 'flag='=>'string'], +'Yaf_Request_Abstract::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getActionName' => ['?string'], +'Yaf_Request_Abstract::getBaseUri' => ['?string'], +'Yaf_Request_Abstract::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getControllerName' => ['?string'], +'Yaf_Request_Abstract::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getException' => ['?Exception'], +'Yaf_Request_Abstract::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getLanguage' => ['?string'], +'Yaf_Request_Abstract::getMethod' => ['?string'], +'Yaf_Request_Abstract::getModuleName' => ['?string'], +'Yaf_Request_Abstract::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], +'Yaf_Request_Abstract::getParams' => ['?array'], +'Yaf_Request_Abstract::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRaw' => ['?string'], +'Yaf_Request_Abstract::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRequestUri' => ['?string'], +'Yaf_Request_Abstract::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::cleanParams' => ['?Yaf_Request_Abstract'], +'Yaf_Request_Abstract::isCli' => ['bool'], +'Yaf_Request_Abstract::isDelete' => ['bool'], +'Yaf_Request_Abstract::isDispatched' => ['bool'], +'Yaf_Request_Abstract::isGet' => ['bool'], +'Yaf_Request_Abstract::isHead' => ['bool'], +'Yaf_Request_Abstract::isOptions' => ['bool'], +'Yaf_Request_Abstract::isPatch' => ['bool'], +'Yaf_Request_Abstract::isPost' => ['bool'], +'Yaf_Request_Abstract::isPut' => ['bool'], +'Yaf_Request_Abstract::isRouted' => ['bool'], +'Yaf_Request_Abstract::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Abstract::setActionName' => ['?Yaf_Request_Abstract', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setBaseUri' => ['Yaf_Request_Abstract|false', 'uir'=>'string'], +'Yaf_Request_Abstract::setControllerName' => ['?Yaf_Request_Abstract', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setDispatched' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], +'Yaf_Request_Abstract::setModuleName' => ['?Yaf_Request_Abstract', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setParam' => ['Yaf_Request_Abstract|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Abstract::setRequestUri' => ['?Yaf_Request_Abstract', 'uir'=>'string'], +'Yaf_Request_Abstract::setRouted' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], 'Yaf_Request_Http::__clone' => ['void'], -'Yaf_Request_Http::__construct' => ['void'], -'Yaf_Request_Http::get' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getActionName' => ['string'], -'Yaf_Request_Http::getBaseUri' => ['string'], -'Yaf_Request_Http::getControllerName' => ['string'], -'Yaf_Request_Http::getCookie' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getException' => ['Yaf_Exception'], -'Yaf_Request_Http::getFiles' => ['void'], -'Yaf_Request_Http::getLanguage' => ['string'], -'Yaf_Request_Http::getMethod' => ['string'], -'Yaf_Request_Http::getModuleName' => ['string'], +'Yaf_Request_Http::__construct' => ['void', 'requestUri='=>'?string', 'baseUri='=>'?string'], +'Yaf_Request_Http::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getActionName' => ['?string'], +'Yaf_Request_Http::getBaseUri' => ['?string'], +'Yaf_Request_Http::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getControllerName' => ['?string'], +'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getException' => ['?Exception'], +'Yaf_Request_Http::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getLanguage' => ['?string'], +'Yaf_Request_Http::getMethod' => ['?string'], +'Yaf_Request_Http::getModuleName' => ['?string'], 'Yaf_Request_Http::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getParams' => ['array'], -'Yaf_Request_Http::getPost' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getQuery' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getRaw' => ['mixed'], -'Yaf_Request_Http::getRequest' => ['void'], -'Yaf_Request_Http::getRequestUri' => ['string'], -'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Http::getParams' => ['?array'], +'Yaf_Request_Http::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRaw' => ['?string'], +'Yaf_Request_Http::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRequestUri' => ['?string'], +'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::cleanParams' => ['?Yaf_Request_Http'], 'Yaf_Request_Http::isCli' => ['bool'], +'Yaf_Request_Http::isDelete' => ['bool'], 'Yaf_Request_Http::isDispatched' => ['bool'], 'Yaf_Request_Http::isGet' => ['bool'], 'Yaf_Request_Http::isHead' => ['bool'], 'Yaf_Request_Http::isOptions' => ['bool'], +'Yaf_Request_Http::isPatch' => ['bool'], 'Yaf_Request_Http::isPost' => ['bool'], 'Yaf_Request_Http::isPut' => ['bool'], 'Yaf_Request_Http::isRouted' => ['bool'], 'Yaf_Request_Http::isXmlHttpRequest' => ['bool'], -'Yaf_Request_Http::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Http::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Http::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Http::setDispatched' => ['bool'], -'Yaf_Request_Http::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Http::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Http::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Http::setRouted' => ['Yaf_Request_Abstract|bool'], -'Yaf_Request_Simple::__clone' => ['void'], -'Yaf_Request_Simple::__construct' => ['void'], -'Yaf_Request_Simple::get' => ['void'], -'Yaf_Request_Simple::getActionName' => ['string'], -'Yaf_Request_Simple::getBaseUri' => ['string'], -'Yaf_Request_Simple::getControllerName' => ['string'], -'Yaf_Request_Simple::getCookie' => ['void'], -'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getException' => ['Yaf_Exception'], -'Yaf_Request_Simple::getFiles' => ['void'], -'Yaf_Request_Simple::getLanguage' => ['string'], -'Yaf_Request_Simple::getMethod' => ['string'], -'Yaf_Request_Simple::getModuleName' => ['string'], +'Yaf_Request_Http::setActionName' => ['?Yaf_Request_Http', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setBaseUri' => ['Yaf_Request_Http|false', 'uir'=>'string'], +'Yaf_Request_Http::setControllerName' => ['?Yaf_Request_Http', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setDispatched' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Http::setModuleName' => ['?Yaf_Request_Http', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setParam' => ['Yaf_Request_Http|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Http::setRequestUri' => ['?Yaf_Request_Http', 'uir'=>'string'], +'Yaf_Request_Http::setRouted' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Simple::__construct' => ['void', 'method='=>'?string', 'module='=>'?string', 'controller='=>'?string', 'action='=>'?string', 'params='=>'?array'], +'Yaf_Request_Simple::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getActionName' => ['?string'], +'Yaf_Request_Simple::getBaseUri' => ['?string'], +'Yaf_Request_Simple::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getControllerName' => ['?string'], +'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getException' => ['?Exception'], +'Yaf_Request_Simple::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getLanguage' => ['?string'], +'Yaf_Request_Simple::getMethod' => ['?string'], +'Yaf_Request_Simple::getModuleName' => ['?string'], 'Yaf_Request_Simple::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getParams' => ['array'], -'Yaf_Request_Simple::getPost' => ['void'], -'Yaf_Request_Simple::getQuery' => ['void'], -'Yaf_Request_Simple::getRequest' => ['void'], -'Yaf_Request_Simple::getRequestUri' => ['string'], -'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Simple::getParams' => ['?array'], +'Yaf_Request_Simple::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRaw' => ['?string'], +'Yaf_Request_Simple::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRequestUri' => ['?string'], +'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::cleanParams' => ['?Yaf_Request_Simple'], 'Yaf_Request_Simple::isCli' => ['bool'], +'Yaf_Request_Simple::isDelete' => ['bool'], 'Yaf_Request_Simple::isDispatched' => ['bool'], 'Yaf_Request_Simple::isGet' => ['bool'], 'Yaf_Request_Simple::isHead' => ['bool'], 'Yaf_Request_Simple::isOptions' => ['bool'], +'Yaf_Request_Simple::isPatch' => ['bool'], 'Yaf_Request_Simple::isPost' => ['bool'], 'Yaf_Request_Simple::isPut' => ['bool'], 'Yaf_Request_Simple::isRouted' => ['bool'], -'Yaf_Request_Simple::isXmlHttpRequest' => ['void'], -'Yaf_Request_Simple::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Simple::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Simple::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Simple::setDispatched' => ['bool'], -'Yaf_Request_Simple::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Simple::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Simple::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Simple::setRouted' => ['Yaf_Request_Abstract|bool'], +'Yaf_Request_Simple::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Simple::setActionName' => ['?Yaf_Request_Simple', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setBaseUri' => ['Yaf_Request_Simple|false', 'uir'=>'string'], +'Yaf_Request_Simple::setControllerName' => ['?Yaf_Request_Simple', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setDispatched' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], +'Yaf_Request_Simple::setModuleName' => ['?Yaf_Request_Simple', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setParam' => ['Yaf_Request_Simple|bool|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Simple::setRequestUri' => ['?Yaf_Request_Simple', 'uir'=>'string'], +'Yaf_Request_Simple::setRouted' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], 'Yaf_Response_Abstract::__clone' => ['void'], 'Yaf_Response_Abstract::__construct' => ['void'], 'Yaf_Response_Abstract::__destruct' => ['void'], 'Yaf_Response_Abstract::__toString' => ['string'], -'Yaf_Response_Abstract::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Abstract::clearHeaders' => ['void'], -'Yaf_Response_Abstract::getBody' => ['mixed', 'key='=>'string'], -'Yaf_Response_Abstract::getHeader' => ['void'], -'Yaf_Response_Abstract::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::response' => ['void'], -'Yaf_Response_Abstract::setAllHeaders' => ['void'], -'Yaf_Response_Abstract::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::setHeader' => ['void'], -'Yaf_Response_Abstract::setRedirect' => ['void'], -'Yaf_Response_Cli::__clone' => [''], +'Yaf_Response_Abstract::appendBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::clearBody' => ['?Yaf_Response_Abstract', 'name='=>'?string'], +'Yaf_Response_Abstract::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Abstract::prependBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::response' => ['bool'], +'Yaf_Response_Abstract::setBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Cli::__clone' => ['void'], 'Yaf_Response_Cli::__construct' => ['void'], -'Yaf_Response_Cli::__destruct' => [''], +'Yaf_Response_Cli::__destruct' => ['void'], 'Yaf_Response_Cli::__toString' => ['string'], -'Yaf_Response_Cli::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Cli::getBody' => ['mixed', 'key='=>'null|string'], -'Yaf_Response_Cli::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::__clone' => [''], +'Yaf_Response_Cli::appendBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::clearBody' => ['?Yaf_Response_Cli', 'name='=>'?string'], +'Yaf_Response_Cli::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Cli::prependBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::response' => ['bool'], +'Yaf_Response_Cli::setBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Http::__clone' => ['void'], 'Yaf_Response_Http::__construct' => ['void'], -'Yaf_Response_Http::__destruct' => [''], +'Yaf_Response_Http::__destruct' => ['void'], 'Yaf_Response_Http::__toString' => ['string'], -'Yaf_Response_Http::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Abstract|false', 'name='=>'string'], -'Yaf_Response_Http::getBody' => ['mixed', 'key='=>'null|string'], +'Yaf_Response_Http::appendBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Http|false|null'], +'Yaf_Response_Http::clearBody' => ['?Yaf_Response_Http', 'name='=>'?string'], +'Yaf_Response_Http::getBody' => ['mixed', 'name='=>'string'], 'Yaf_Response_Http::getHeader' => ['mixed', 'name='=>'string'], -'Yaf_Response_Http::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::response' => ['bool'], +'Yaf_Response_Http::prependBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::response' => ['?bool'], 'Yaf_Response_Http::setAllHeaders' => ['bool', 'headers'=>'array'], -'Yaf_Response_Http::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::setHeader' => ['bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], -'Yaf_Response_Http::setRedirect' => ['bool', 'url'=>'string'], +'Yaf_Response_Http::setBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::setHeader' => ['?bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], +'Yaf_Response_Http::setRedirect' => ['?bool', 'url'=>'string'], 'Yaf_Route_Interface::__construct' => ['void'], 'Yaf_Route_Interface::assemble' => ['string', 'info'=>'array', 'query='=>'array'], 'Yaf_Route_Interface::route' => ['bool', 'request'=>'Yaf_Request_Abstract'], @@ -13561,7 +12232,7 @@ 'Yaf_Session::__get' => ['void', 'name'=>'string'], 'Yaf_Session::__isset' => ['void', 'name'=>'string'], 'Yaf_Session::__set' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Session::__sleep' => ['void'], +'Yaf_Session::__sleep' => ['list'], 'Yaf_Session::__unset' => ['void', 'name'=>'string'], 'Yaf_Session::__wakeup' => ['void'], 'Yaf_Session::count' => ['0|positive-int'], @@ -13580,23 +12251,24 @@ 'Yaf_Session::set' => ['Yaf_Session|bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Session::start' => ['void'], 'Yaf_Session::valid' => ['void'], -'Yaf_View_Interface::assign' => ['bool', 'name'=>'string', 'value='=>'string'], -'Yaf_View_Interface::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::getScriptPath' => ['void'], -'Yaf_View_Interface::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::setScriptPath' => ['void', 'template_dir'=>'string'], -'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'array'], -'Yaf_View_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_View_Simple::__isset' => ['void', 'name'=>'string'], +'Yaf_View_Interface::assign' => ['Yaf_View_Interface|bool', 'name'=>'string', 'value='=>'?mixed'], +'Yaf_View_Interface::display' => ['Yaf_View_Interface|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::getScriptPath' => ['string'], +'Yaf_View_Interface::render' => ['string|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'?array'], +'Yaf_View_Simple::__get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::__isset' => ['bool', 'name'=>'string'], 'Yaf_View_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], -'Yaf_View_Simple::assign' => ['bool', 'name'=>'string', 'value='=>'mixed'], -'Yaf_View_Simple::assignRef' => ['bool', 'name'=>'string', '&rw_value'=>'mixed'], -'Yaf_View_Simple::clear' => ['bool', 'name='=>'string'], -'Yaf_View_Simple::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::eval' => ['string', 'tpl_content'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::getScriptPath' => ['string'], -'Yaf_View_Simple::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::assign' => ['Yaf_View_Simple|false|null', 'name='=>'?mixed', 'default='=>'?mixed'], +'Yaf_View_Simple::assignRef' => ['?Yaf_View_Simple', 'name'=>'string', '&value'=>'mixed'], +'Yaf_View_Simple::clear' => ['?Yaf_View_Simple', 'name='=>'string'], +'Yaf_View_Simple::display' => ['?bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::eval' => ['string|null|false', 'tpl_str'=>'string', 'vars='=>'?array'], +'Yaf_View_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::getScriptPath' => ['?string'], +'Yaf_View_Simple::render' => ['string|null|false', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::setScriptPath' => ['Yaf_View_Simple|false|null', 'template_dir'=>'string'], 'yaml_emit' => ['string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_emit_file' => ['bool', 'filename'=>'string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_parse' => ['mixed', 'input'=>'string', 'pos='=>'int', '&w_ndocs='=>'int', 'callbacks='=>'array'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php new file mode 100644 index 0000000000..92f41e9db0 --- /dev/null +++ b/resources/functionMap_bleedingEdge.php @@ -0,0 +1,8 @@ + [ + ], + 'old' => [ + ], +]; diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index 73832288f7..86bc99ef80 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -22,35 +22,36 @@ */ return [ 'new' => [ - 'FFI::addr' => ['FFI\CData', '&ptr'=>'FFI\CData'], - 'FFI::alignof' => ['int', '&ptr'=>'mixed'], + 'FFI::addr' => ['FFI\CData', 'ptr'=>'FFI\CData'], + 'FFI::alignof' => ['int', 'ptr'=>'mixed'], 'FFI::arrayType' => ['FFI\CType', 'type'=>'string|FFI\CType', 'dims'=>'array'], - 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', '&ptr'=>''], + 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', 'ptr'=>''], 'FFI::cdef' => ['FFI', 'code='=>'string', 'lib='=>'?string'], - 'FFI::free' => ['void', '&ptr'=>'FFI\CData'], + 'FFI::free' => ['void', 'ptr'=>'FFI\CData'], 'FFI::load' => ['FFI', 'filename'=>'string'], - 'FFI::memcmp' => ['int', '&ptr1'=>'FFI\CData|string', '&ptr2'=>'FFI\CData|string', 'size'=>'int'], - 'FFI::memcpy' => ['void', '&dst'=>'FFI\CData', '&src'=>'string|FFI\CData', 'size'=>'int'], - 'FFI::memset' => ['void', '&ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], + 'FFI::memcmp' => ['int', 'ptr1'=>'FFI\CData|string', 'ptr2'=>'FFI\CData|string', 'size'=>'int'], + 'FFI::memcpy' => ['void', 'dst'=>'FFI\CData', 'src'=>'string|FFI\CData', 'size'=>'int'], + 'FFI::memset' => ['void', 'ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], 'FFI::new' => ['FFI\CData', 'type'=>'string|FFI\CType', 'owned='=>'bool', 'persistent='=>'bool'], 'FFI::scope' => ['FFI', 'scope_name'=>'string'], - 'FFI::sizeof' => ['int', '&ptr'=>'FFI\CData|FFI\CType'], - 'FFI::string' => ['string', '&ptr'=>'FFI\CData', 'size='=>'int'], - 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], + 'FFI::sizeof' => ['int', 'ptr'=>'FFI\CData|FFI\CType'], + 'FFI::string' => ['string', 'ptr'=>'FFI\CData', 'size='=>'int'], + 'FFI::typeof' => ['FFI\CType', 'ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], + 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'positive-int'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], - 'mb_str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], - 'password_algos' => ['array'], - 'password_hash' => ['string|false', 'password'=>'string', 'algo'=>'string|null', 'options='=>'array'], + 'mb_str_split' => ['list|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], + 'password_algos' => ['list'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'string|null', 'options='=>'array'], - 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], + 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], - 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable', 'add='=>'bool'], + 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable(int):void', 'add='=>'bool'], 'ReflectionProperty::getType' => ['?ReflectionType'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isInitialized' => ['bool', 'object='=>'?object'], 'ReflectionReference::fromArrayElement' => ['?ReflectionReference', 'array'=>'array', 'key'=>'int|string'], 'ReflectionReference::getId' => ['string'], + 'SplFileObject::fwrite' => ['int|false', 'str'=>'string', 'length='=>'int'], 'SQLite3Stmt::getSQL' => ['string', 'expanded='=>'bool'], 'strip_tags' => ['string', 'str'=>'string', 'allowable_tags='=>'string|array'], 'WeakReference::create' => ['WeakReference', 'referent'=>'object'], @@ -59,5 +60,6 @@ ], 'old' => [ 'implode\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], + 'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], ], ]; diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index f6a4efdd98..371175e58b 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -22,12 +22,16 @@ return [ 'new' => [ 'array_combine' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], - 'array_fill' => ['array', 'start_key'=>'int', 'num'=>'0|positive-int', 'val'=>'mixed'], + 'base64_decode' => ['string', 'string'=>'string', 'strict='=>'false'], + 'base64_decode\'1' => ['string|false', 'string'=>'string', 'strict='=>'true'], 'bcdiv' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], + 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], + 'ceil' => ['float', 'number'=>'float'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], 'count_chars' => ['array|string', 'input'=>'string', 'mode='=>'int'], + 'curl_init' => ['__benevolent', 'url='=>'string'], 'date_add' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], 'date_date_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'date_diff' => ['DateInterval', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], @@ -35,19 +39,28 @@ 'date_isodate_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array', 'date'=>'string'], 'date_sub' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['array{sunrise: int|bool,sunset: int|bool,transit: int|bool,civil_twilight_begin: int|bool,civil_twilight_end: int|bool,nautical_twilight_begin: int|bool,nautical_twilight_end: int|bool,astronomical_twilight_begin: int|bool,astronomical_twilight_end: int|bool}', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], - 'explode' => ['non-empty-array', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], + 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], + 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'fgetcsv' => ['non-empty-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_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'positive-int', 'length='=>'0|positive-int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], - 'imagecreate' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreate' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd2' => ['false|object', 'filename'=>'string'], @@ -60,7 +73,7 @@ 'imagecreatefromwebp' => ['false|object', 'filename'=>'string'], 'imagecreatefromxbm' => ['false|object', 'filename'=>'string'], 'imagecreatefromxpm' => ['false|object', 'filename'=>'string'], - 'imagecreatetruecolor' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreatetruecolor' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], 'imagecrop' => ['false|object', 'im'=>'resource', 'rect'=>'array'], 'imagecropauto' => ['false|object', 'im'=>'resource', 'mode'=>'int', 'threshold'=>'float', 'color'=>'int'], 'imagegetclip' => ['array', 'im'=>'resource'], @@ -69,33 +82,50 @@ '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'], + 'getenv' => ['string|false', 'varname'=>'string', 'local_only='=>'bool'], + 'getenv\'1' => ['array', 'varname='=>'null', 'local_only='=>'bool'], + 'ldap_set_rebind_proc' => ['bool', 'ldap'=>'resource', 'callback'=>'?callable'], 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string'], - 'mb_str_split' => ['non-empty-array', 'str'=>'string', 'split_length='=>'positive-int', 'encoding='=>'string'], + 'mb_detect_order' => ['bool', 'encoding_list'=>'non-empty-list|non-falsy-string'], + 'mb_detect_order\'1' => ['list', 'encoding_list='=>'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'], + 'odbc_do' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'], - 'password_hash' => ['string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], + 'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], + 'PDOStatement::fetchAll' => ['array', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], 'PhpToken::isIgnorable' => ['bool'], - 'PhpToken::getTokenName' => ['string'], + 'PhpToken::getTokenName' => ['non-falsy-string'], + 'preg_match_all' => ['0|positive-int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'], + 'scandir' => ['__benevolent|false>', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING|SCANDIR_SORT_NONE', 'context='=>'?resource'], + 'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'string|null', 'domain='=>'string|null', 'secure='=>'bool|null', 'httponly='=>'bool|null'], + 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string|null,domain?:string|null,secure?:bool|null,httponly?:bool|null,samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], + 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], 'socket_addrinfo_lookup' => ['AddressInfo[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], - 'socket_select' => ['int|false', '&rw_read'=>'Socket[]|null', '&rw_write'=>'Socket[]|null', '&rw_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], + 'socket_select' => ['int|false', '&w_read'=>'Socket[]|null', '&w_write'=>'Socket[]|null', '&w_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'spl_autoload_functions' => ['list'], 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], - 'str_split' => ['non-empty-array', 'str'=>'string', 'split_length='=>'positive-int'], + 'str_split' => ['non-empty-list', 'str'=>'string', 'split_length='=>'positive-int'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_starts_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], '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'], - 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], + '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'], @@ -128,7 +158,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], - 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'XMLWriter', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string'], @@ -145,14 +175,15 @@ 'xmlwriter_write_raw' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], ], 'old' => [ - 'array_combine' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], - 'array_fill' => ['array', 'start_key'=>'int', 'num'=>'int', 'val'=>'mixed'], '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'], @@ -160,16 +191,24 @@ 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array|false', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array|false', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'each' => ['array{0:int|string,key:int|string,1:mixed,value:mixed}', '&r_arr'=>'array'], + 'ezmlm_hash' => ['int', 'addr'=>'string'], + 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], + 'floor' => ['__benevolent', 'number'=>'float'], + 'get_magic_quotes_gpc' => ['false'], 'gmdate' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], - 'hash_hkdf' => ['string|false', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'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'], @@ -194,29 +233,40 @@ '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' => ['string|false|null', 'password'=>'string', 'algo'=>'?string|?int', 'options='=>'array'], + 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], - 'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], + 'restore_include_path' => ['void'], + 'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], + '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,samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], + 'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['?string|?false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int'], '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'], - 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], + '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'], @@ -249,7 +299,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], - 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], diff --git a/resources/functionMap_php80delta_bleedingEdge.php b/resources/functionMap_php80delta_bleedingEdge.php new file mode 100644 index 0000000000..92f41e9db0 --- /dev/null +++ b/resources/functionMap_php80delta_bleedingEdge.php @@ -0,0 +1,8 @@ + [ + ], + 'old' => [ + ], +]; diff --git a/resources/functionMap_php81delta.php b/resources/functionMap_php81delta.php new file mode 100644 index 0000000000..2fca7f239b --- /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|string|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], + 'pg_lo_import\'1' => ['int|string', '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..c32bee4c44 --- /dev/null +++ b/resources/functionMap_php83delta.php @@ -0,0 +1,33 @@ + [ + '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}'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMap_php84delta.php b/resources/functionMap_php84delta.php new file mode 100644 index 0000000000..d5552a0840 --- /dev/null +++ b/resources/functionMap_php84delta.php @@ -0,0 +1,27 @@ + [ + 'bcround' => ['numeric-string', 'num'=>'numeric-string', 'precision='=>'int', 'mode='=>'RoundingMode'], + 'http_get_last_response_headers' => ['list|null'], + 'http_clear_last_response_headers' => ['void'], + 'mb_lcfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + 'mb_ucfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index f136e361e4..b58fa77c7f 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1,6 +1,22 @@ true as a modification to bin/functionMetadata_original.php. + * 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs + * 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php. + */ + return [ + 'BackedEnum::from' => ['hasSideEffects' => false], + 'BackedEnum::tryFrom' => ['hasSideEffects' => false], 'CURLFile::getFilename' => ['hasSideEffects' => false], 'CURLFile::getMimeType' => ['hasSideEffects' => false], 'CURLFile::getPostFilename' => ['hasSideEffects' => false], @@ -28,6 +44,8 @@ 'Cassandra\\Exception\\UnpreparedException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\ValidationException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\WriteTimeoutException::__construct' => ['hasSideEffects' => false], + 'Closure::bind' => ['hasSideEffects' => false], + 'Closure::bindTo' => ['hasSideEffects' => false], 'Collator::__construct' => ['hasSideEffects' => false], 'Collator::compare' => ['hasSideEffects' => false], 'Collator::getAttribute' => ['hasSideEffects' => false], @@ -68,6 +86,12 @@ 'DateTimeImmutable::setTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::setTimezone' => ['hasSideEffects' => false], 'DateTimeImmutable::sub' => ['hasSideEffects' => false], + 'DateTimeInterface::diff' => ['hasSideEffects' => false], + 'DateTimeInterface::format' => ['hasSideEffects' => false], + 'DateTimeInterface::getOffset' => ['hasSideEffects' => false], + 'DateTimeInterface::getTimestamp' => ['hasSideEffects' => false], + 'DateTimeInterface::getTimezone' => ['hasSideEffects' => false], + 'Error::__construct' => ['hasSideEffects' => false], 'ErrorException::__construct' => ['hasSideEffects' => false], 'Event::__construct' => ['hasSideEffects' => false], 'EventBase::getFeatures' => ['hasSideEffects' => false], @@ -86,6 +110,7 @@ 'EventHttpConnection::__construct' => ['hasSideEffects' => false], 'EventHttpRequest::__construct' => ['hasSideEffects' => false], 'EventHttpRequest::getCommand' => ['hasSideEffects' => false], + 'EventHttpRequest::getConnection' => ['hasSideEffects' => false], 'EventHttpRequest::getHost' => ['hasSideEffects' => false], 'EventHttpRequest::getInputBuffer' => ['hasSideEffects' => false], 'EventHttpRequest::getInputHeaders' => ['hasSideEffects' => false], @@ -338,6 +363,8 @@ 'ImagickPixelIterator::getIteratorRow' => ['hasSideEffects' => false], 'ImagickPixelIterator::getNextIteratorRow' => ['hasSideEffects' => false], 'ImagickPixelIterator::getPreviousIteratorRow' => ['hasSideEffects' => false], + 'IntBackedEnum::from' => ['hasSideEffects' => false], + 'IntBackedEnum::tryFrom' => ['hasSideEffects' => false], 'IntlBreakIterator::current' => ['hasSideEffects' => false], 'IntlBreakIterator::getErrorCode' => ['hasSideEffects' => false], 'IntlBreakIterator::getErrorMessage' => ['hasSideEffects' => false], @@ -425,6 +452,8 @@ 'NumberFormatter::getPattern' => ['hasSideEffects' => false], 'NumberFormatter::getSymbol' => ['hasSideEffects' => false], 'NumberFormatter::getTextAttribute' => ['hasSideEffects' => false], + 'Redis::connect' => ['hasSideEffects' => true], + 'Redis::pconnect' => ['hasSideEffects' => true], 'ReflectionAttribute::getArguments' => ['hasSideEffects' => false], 'ReflectionAttribute::getName' => ['hasSideEffects' => false], 'ReflectionAttribute::getTarget' => ['hasSideEffects' => false], @@ -468,6 +497,7 @@ 'ReflectionClass::isInternal' => ['hasSideEffects' => false], 'ReflectionClass::isIterable' => ['hasSideEffects' => false], 'ReflectionClass::isIterateable' => ['hasSideEffects' => false], + 'ReflectionClass::isReadOnly' => ['hasSideEffects' => false], 'ReflectionClass::isSubclassOf' => ['hasSideEffects' => false], 'ReflectionClass::isTrait' => ['hasSideEffects' => false], 'ReflectionClass::isUserDefined' => ['hasSideEffects' => false], @@ -480,6 +510,9 @@ 'ReflectionClassConstant::isPrivate' => ['hasSideEffects' => false], 'ReflectionClassConstant::isProtected' => ['hasSideEffects' => false], 'ReflectionClassConstant::isPublic' => ['hasSideEffects' => false], + 'ReflectionEnumBackedCase::getBackingValue' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getEnum' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getValue' => ['hasSideEffects' => false], 'ReflectionExtension::getClassNames' => ['hasSideEffects' => false], 'ReflectionExtension::getClasses' => ['hasSideEffects' => false], 'ReflectionExtension::getConstants' => ['hasSideEffects' => false], @@ -493,8 +526,10 @@ '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], @@ -509,10 +544,13 @@ 'ReflectionFunctionAbstract::getShortName' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getStartLine' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getStaticVariables' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getTentativeReturnType' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isClosure' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isDeprecated' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isGenerator' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isInternal' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isStatic' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isUserDefined' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isVariadic' => ['hasSideEffects' => false], 'ReflectionGenerator::getExecutingFile' => ['hasSideEffects' => false], @@ -521,6 +559,7 @@ 'ReflectionGenerator::getFunction' => ['hasSideEffects' => false], 'ReflectionGenerator::getThis' => ['hasSideEffects' => false], 'ReflectionGenerator::getTrace' => ['hasSideEffects' => false], + 'ReflectionIntersectionType::getTypes' => ['hasSideEffects' => false], 'ReflectionMethod::getClosure' => ['hasSideEffects' => false], 'ReflectionMethod::getDeclaringClass' => ['hasSideEffects' => false], 'ReflectionMethod::getModifiers' => ['hasSideEffects' => false], @@ -533,6 +572,7 @@ 'ReflectionMethod::isProtected' => ['hasSideEffects' => false], 'ReflectionMethod::isPublic' => ['hasSideEffects' => false], 'ReflectionMethod::isStatic' => ['hasSideEffects' => false], + 'ReflectionMethod::setAccessible' => ['hasSideEffects' => false], 'ReflectionNamedType::getName' => ['hasSideEffects' => false], 'ReflectionNamedType::isBuiltin' => ['hasSideEffects' => false], 'ReflectionParameter::getAttributes' => ['hasSideEffects' => false], @@ -567,6 +607,7 @@ 'ReflectionProperty::isProtected' => ['hasSideEffects' => false], 'ReflectionProperty::isPublic' => ['hasSideEffects' => false], 'ReflectionProperty::isStatic' => ['hasSideEffects' => false], + 'ReflectionProperty::setAccessible' => ['hasSideEffects' => false], 'ReflectionReference::getId' => ['hasSideEffects' => false], 'ReflectionType::isBuiltin' => ['hasSideEffects' => false], 'ReflectionUnionType::getTypes' => ['hasSideEffects' => false], @@ -600,16 +641,40 @@ 'SimpleXMLIterator::hasChildren' => ['hasSideEffects' => false], 'SimpleXMLIterator::valid' => ['hasSideEffects' => false], 'SoapFault::__construct' => ['hasSideEffects' => false], + 'SplDoublyLinkedList::pop' => ['hasSideEffects' => true], + 'SplDoublyLinkedList::shift' => ['hasSideEffects' => true], + '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], + 'SplFixedArray::extract' => ['hasSideEffects' => true], + 'SplHead::extract' => ['hasSideEffects' => true], + 'SplHead::insert' => ['hasSideEffects' => true], + 'SplHead::recoverFromCorruption' => ['hasSideEffects' => true], + 'SplObjectStorage::addAll' => ['hasSideEffects' => true], + 'SplObjectStorage::attach' => ['hasSideEffects' => true], + 'SplObjectStorage::detach' => ['hasSideEffects' => true], + 'SplObjectStorage::removeAll' => ['hasSideEffects' => true], + 'SplObjectStorage::removeAllExcept' => ['hasSideEffects' => true], + 'SplPriorityQueue::extract' => ['hasSideEffects' => true], + 'SplPriorityQueue::insert' => ['hasSideEffects' => true], + 'SplPriorityQueue::recoverFromCorruption' => ['hasSideEffects' => true], + 'SplQueue::dequeue' => ['hasSideEffects' => true], 'Spoofchecker::__construct' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::getFQN' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::getTypeNameFromNode' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::hasMutedProblem' => ['hasSideEffects' => false], - 'StubTests\\Model\\StubsContainer::getClass' => ['hasSideEffects' => false], - 'StubTests\\Model\\StubsContainer::getInterface' => ['hasSideEffects' => false], + 'StringBackedEnum::from' => ['hasSideEffects' => false], + 'StringBackedEnum::tryFrom' => ['hasSideEffects' => false], + 'StubTests\\CodeStyle\\BracesOneLineFixer::getDefinition' => ['hasSideEffects' => false], 'StubTests\\Parsers\\ExpectedFunctionArgumentsInfo::__toString' => ['hasSideEffects' => false], - 'StubTests\\Parsers\\Visitors\\CoreStubASTVisitor::__construct' => ['hasSideEffects' => false], + 'StubTests\\StubsMetaExpectedArgumentsTest::getClassMemberFqn' => ['hasSideEffects' => false], 'StubTests\\StubsParameterNamesTest::printParameters' => ['hasSideEffects' => false], - 'StubTests\\StubsTest::getParameterRepresentation' => ['hasSideEffects' => false], 'Transliterator::createInverse' => ['hasSideEffects' => false], 'Transliterator::getErrorCode' => ['hasSideEffects' => false], 'Transliterator::getErrorMessage' => ['hasSideEffects' => false], @@ -625,6 +690,15 @@ 'UConverter::getStandards' => ['hasSideEffects' => false], 'UConverter::getSubstChars' => ['hasSideEffects' => false], 'UConverter::reasonText' => ['hasSideEffects' => false], + 'UnitEnum::cases' => ['hasSideEffects' => false], + 'WeakMap::count' => ['hasSideEffects' => false], + 'WeakMap::getIterator' => ['hasSideEffects' => false], + 'WeakMap::offsetExists' => ['hasSideEffects' => false], + 'WeakMap::offsetGet' => ['hasSideEffects' => false], + 'WeakReference::create' => ['hasSideEffects' => false], + 'WeakReference::get' => ['hasSideEffects' => false], + 'XmlReader::next' => ['hasSideEffects' => true], + 'XmlReader::read' => ['hasSideEffects' => true], 'Zookeeper::getAcl' => ['hasSideEffects' => false], 'Zookeeper::getChildren' => ['hasSideEffects' => false], 'Zookeeper::getClientId' => ['hasSideEffects' => false], @@ -658,6 +732,7 @@ 'array_intersect_key' => ['hasSideEffects' => false], 'array_intersect_uassoc' => ['hasSideEffects' => false], 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_is_list' => ['hasSideEffects' => false], 'array_key_exists' => ['hasSideEffects' => false], 'array_key_first' => ['hasSideEffects' => false], 'array_key_last' => ['hasSideEffects' => false], @@ -665,12 +740,15 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], 'array_search' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -680,6 +758,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -711,27 +790,31 @@ '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' => false], + 'cli_get_process_title' => ['hasSideEffects' => true], 'collator_compare' => ['hasSideEffects' => false], 'collator_create' => ['hasSideEffects' => false], 'collator_get_attribute' => ['hasSideEffects' => false], - 'collator_get_error_code' => ['hasSideEffects' => false], + 'collator_get_error_code' => ['hasSideEffects' => true], 'collator_get_error_message' => ['hasSideEffects' => false], 'collator_get_locale' => ['hasSideEffects' => false], 'collator_get_sort_key' => ['hasSideEffects' => false], 'collator_get_strength' => ['hasSideEffects' => false], 'compact' => ['hasSideEffects' => false], - 'connection_aborted' => ['hasSideEffects' => false], - 'connection_status' => ['hasSideEffects' => false], - 'constant' => ['hasSideEffects' => false], + 'connection_aborted' => ['hasSideEffects' => true], + 'connection_status' => ['hasSideEffects' => true], + 'constant' => ['hasSideEffects' => true], 'convert_cyr_string' => ['hasSideEffects' => false], 'convert_uudecode' => ['hasSideEffects' => false], 'convert_uuencode' => ['hasSideEffects' => false], + 'copy' => ['hasSideEffects' => true], 'cos' => ['hasSideEffects' => false], 'cosh' => ['hasSideEffects' => false], 'count' => ['hasSideEffects' => false], @@ -750,47 +833,47 @@ 'ctype_upper' => ['hasSideEffects' => false], 'ctype_xdigit' => ['hasSideEffects' => false], 'curl_copy_handle' => ['hasSideEffects' => false], - 'curl_errno' => ['hasSideEffects' => false], - 'curl_error' => ['hasSideEffects' => false], + 'curl_errno' => ['hasSideEffects' => true], + 'curl_error' => ['hasSideEffects' => true], 'curl_escape' => ['hasSideEffects' => false], 'curl_file_create' => ['hasSideEffects' => false], - 'curl_getinfo' => ['hasSideEffects' => false], - 'curl_multi_errno' => ['hasSideEffects' => false], + 'curl_getinfo' => ['hasSideEffects' => true], + 'curl_multi_errno' => ['hasSideEffects' => true], 'curl_multi_getcontent' => ['hasSideEffects' => false], 'curl_multi_info_read' => ['hasSideEffects' => false], - 'curl_share_errno' => ['hasSideEffects' => false], + 'curl_share_errno' => ['hasSideEffects' => true], 'curl_share_strerror' => ['hasSideEffects' => false], 'curl_strerror' => ['hasSideEffects' => false], 'curl_unescape' => ['hasSideEffects' => false], 'curl_version' => ['hasSideEffects' => false], 'current' => ['hasSideEffects' => false], - 'date' => ['hasSideEffects' => false], - 'date_create' => ['hasSideEffects' => false], - 'date_create_from_format' => ['hasSideEffects' => false], - 'date_create_immutable' => ['hasSideEffects' => false], - 'date_create_immutable_from_format' => ['hasSideEffects' => false], + 'date' => ['hasSideEffects' => true], + 'date_create' => ['hasSideEffects' => true], + 'date_create_from_format' => ['hasSideEffects' => true], + 'date_create_immutable' => ['hasSideEffects' => true], + 'date_create_immutable_from_format' => ['hasSideEffects' => true], 'date_default_timezone_get' => ['hasSideEffects' => false], - 'date_diff' => ['hasSideEffects' => false], - 'date_format' => ['hasSideEffects' => false], - 'date_get_last_errors' => ['hasSideEffects' => false], - 'date_interval_create_from_date_string' => ['hasSideEffects' => false], - 'date_interval_format' => ['hasSideEffects' => false], - 'date_offset_get' => ['hasSideEffects' => false], - 'date_parse' => ['hasSideEffects' => false], - 'date_parse_from_format' => ['hasSideEffects' => false], - 'date_sun_info' => ['hasSideEffects' => false], - 'date_sunrise' => ['hasSideEffects' => false], - 'date_sunset' => ['hasSideEffects' => false], - 'date_timestamp_get' => ['hasSideEffects' => false], - 'date_timezone_get' => ['hasSideEffects' => false], + 'date_diff' => ['hasSideEffects' => true], + 'date_format' => ['hasSideEffects' => true], + 'date_get_last_errors' => ['hasSideEffects' => true], + 'date_interval_create_from_date_string' => ['hasSideEffects' => true], + 'date_interval_format' => ['hasSideEffects' => true], + 'date_offset_get' => ['hasSideEffects' => true], + 'date_parse' => ['hasSideEffects' => true], + 'date_parse_from_format' => ['hasSideEffects' => true], + 'date_sun_info' => ['hasSideEffects' => true], + 'date_sunrise' => ['hasSideEffects' => true], + 'date_sunset' => ['hasSideEffects' => true], + 'date_timestamp_get' => ['hasSideEffects' => true], + 'date_timezone_get' => ['hasSideEffects' => true], 'datefmt_create' => ['hasSideEffects' => false], 'datefmt_format' => ['hasSideEffects' => false], 'datefmt_format_object' => ['hasSideEffects' => false], 'datefmt_get_calendar' => ['hasSideEffects' => false], 'datefmt_get_calendar_object' => ['hasSideEffects' => false], 'datefmt_get_datetype' => ['hasSideEffects' => false], - 'datefmt_get_error_code' => ['hasSideEffects' => false], - 'datefmt_get_error_message' => ['hasSideEffects' => false], + 'datefmt_get_error_code' => ['hasSideEffects' => true], + 'datefmt_get_error_message' => ['hasSideEffects' => true], 'datefmt_get_locale' => ['hasSideEffects' => false], 'datefmt_get_pattern' => ['hasSideEffects' => false], 'datefmt_get_timetype' => ['hasSideEffects' => false], @@ -798,26 +881,39 @@ 'datefmt_get_timezone_id' => ['hasSideEffects' => false], 'datefmt_is_lenient' => ['hasSideEffects' => false], 'dcngettext' => ['hasSideEffects' => false], + 'debug_backtrace' => ['hasSideEffects' => true], 'decbin' => ['hasSideEffects' => false], 'dechex' => ['hasSideEffects' => false], 'decoct' => ['hasSideEffects' => false], - 'defined' => ['hasSideEffects' => false], + 'defined' => ['hasSideEffects' => true], 'deflate_init' => ['hasSideEffects' => false], 'deg2rad' => ['hasSideEffects' => false], 'dirname' => ['hasSideEffects' => false], - 'disk_free_space' => ['hasSideEffects' => false], - 'diskfreespace' => ['hasSideEffects' => false], + 'disk_free_space' => ['hasSideEffects' => true], + 'disk_total_space' => ['hasSideEffects' => true], + 'diskfreespace' => ['hasSideEffects' => true], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], - 'error_get_last' => ['hasSideEffects' => false], + 'error_get_last' => ['hasSideEffects' => true], + 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], 'escapeshellcmd' => ['hasSideEffects' => false], 'exp' => ['hasSideEffects' => false], '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], @@ -837,15 +933,28 @@ 'finfo::buffer' => ['hasSideEffects' => false], 'finfo::file' => ['hasSideEffects' => false], 'floatval' => ['hasSideEffects' => false], + 'flock' => ['hasSideEffects' => true], 'floor' => ['hasSideEffects' => false], 'fmod' => ['hasSideEffects' => false], - 'ftok' => ['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], - 'gc_enabled' => ['hasSideEffects' => false], - 'gc_status' => ['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], @@ -862,57 +971,59 @@ 'geoip_region_by_name' => ['hasSideEffects' => false], 'geoip_region_name_by_code' => ['hasSideEffects' => false], 'geoip_time_zone_by_country_and_region' => ['hasSideEffects' => false], - 'get_browser' => ['hasSideEffects' => false], + 'get_browser' => ['hasSideEffects' => true], 'get_called_class' => ['hasSideEffects' => false], 'get_cfg_var' => ['hasSideEffects' => false], 'get_class' => ['hasSideEffects' => false], 'get_class_methods' => ['hasSideEffects' => false], 'get_class_vars' => ['hasSideEffects' => false], - 'get_current_user' => ['hasSideEffects' => false], + 'get_current_user' => ['hasSideEffects' => true], 'get_debug_type' => ['hasSideEffects' => false], - 'get_declared_classes' => ['hasSideEffects' => false], - 'get_declared_interfaces' => ['hasSideEffects' => false], - 'get_declared_traits' => ['hasSideEffects' => false], - 'get_defined_constants' => ['hasSideEffects' => false], - 'get_defined_functions' => ['hasSideEffects' => false], - 'get_defined_vars' => ['hasSideEffects' => false], + 'get_declared_classes' => ['hasSideEffects' => true], + 'get_declared_interfaces' => ['hasSideEffects' => true], + 'get_declared_traits' => ['hasSideEffects' => true], + 'get_defined_constants' => ['hasSideEffects' => true], + 'get_defined_functions' => ['hasSideEffects' => true], + 'get_defined_vars' => ['hasSideEffects' => true], 'get_extension_funcs' => ['hasSideEffects' => false], - 'get_headers' => ['hasSideEffects' => false], + 'get_headers' => ['hasSideEffects' => true], 'get_html_translation_table' => ['hasSideEffects' => false], - 'get_include_path' => ['hasSideEffects' => false], - 'get_included_files' => ['hasSideEffects' => false], + 'get_include_path' => ['hasSideEffects' => true], + 'get_included_files' => ['hasSideEffects' => true], 'get_loaded_extensions' => ['hasSideEffects' => false], - 'get_meta_tags' => ['hasSideEffects' => false], - 'get_object_vars' => ['hasSideEffects' => false], + 'get_meta_tags' => ['hasSideEffects' => true], + 'get_object_vars' => ['hasSideEffects' => true], 'get_parent_class' => ['hasSideEffects' => false], - 'get_required_files' => ['hasSideEffects' => false], + 'get_required_files' => ['hasSideEffects' => true], 'get_resource_id' => ['hasSideEffects' => false], - 'get_resources' => ['hasSideEffects' => false], + 'get_resource_type' => ['hasSideEffects' => true], + 'get_resources' => ['hasSideEffects' => true], 'getallheaders' => ['hasSideEffects' => false], - 'getcwd' => ['hasSideEffects' => false], - 'getdate' => ['hasSideEffects' => false], - 'getenv' => ['hasSideEffects' => false], + 'getcwd' => ['hasSideEffects' => true], + 'getdate' => ['hasSideEffects' => true], + 'getenv' => ['hasSideEffects' => true], 'gethostbyaddr' => ['hasSideEffects' => false], 'gethostbyname' => ['hasSideEffects' => false], 'gethostbynamel' => ['hasSideEffects' => false], 'gethostname' => ['hasSideEffects' => false], - 'getlastmod' => ['hasSideEffects' => false], + 'getlastmod' => ['hasSideEffects' => true], 'getmygid' => ['hasSideEffects' => false], 'getmyinode' => ['hasSideEffects' => false], 'getmypid' => ['hasSideEffects' => false], 'getmyuid' => ['hasSideEffects' => false], + 'getopt' => ['hasSideEffects' => true], 'getprotobyname' => ['hasSideEffects' => false], 'getprotobynumber' => ['hasSideEffects' => false], 'getrandmax' => ['hasSideEffects' => false], - 'getrusage' => ['hasSideEffects' => false], + 'getrusage' => ['hasSideEffects' => true], 'getservbyname' => ['hasSideEffects' => false], 'getservbyport' => ['hasSideEffects' => false], 'gettext' => ['hasSideEffects' => false], - 'gettimeofday' => ['hasSideEffects' => false], + 'gettimeofday' => ['hasSideEffects' => true], 'gettype' => ['hasSideEffects' => false], - 'glob' => ['hasSideEffects' => false], - 'gmdate' => ['hasSideEffects' => false], - 'gmmktime' => ['hasSideEffects' => false], + 'glob' => ['hasSideEffects' => true], + 'gmdate' => ['hasSideEffects' => true], + 'gmmktime' => ['hasSideEffects' => true], 'gmp_abs' => ['hasSideEffects' => false], 'gmp_add' => ['hasSideEffects' => false], 'gmp_and' => ['hasSideEffects' => false], @@ -948,9 +1059,6 @@ 'gmp_pow' => ['hasSideEffects' => false], 'gmp_powm' => ['hasSideEffects' => false], 'gmp_prob_prime' => ['hasSideEffects' => false], - 'gmp_random' => ['hasSideEffects' => false], - 'gmp_random_bits' => ['hasSideEffects' => false], - 'gmp_random_range' => ['hasSideEffects' => false], 'gmp_root' => ['hasSideEffects' => false], 'gmp_rootrem' => ['hasSideEffects' => false], 'gmp_scan0' => ['hasSideEffects' => false], @@ -990,7 +1098,7 @@ 'headers_list' => ['hasSideEffects' => false], 'hebrev' => ['hasSideEffects' => false], 'hexdec' => ['hasSideEffects' => false], - 'hrtime' => ['hasSideEffects' => false], + 'hrtime' => ['hasSideEffects' => true], 'html_entity_decode' => ['hasSideEffects' => false], 'htmlentities' => ['hasSideEffects' => false], 'htmlspecialchars' => ['hasSideEffects' => false], @@ -1028,7 +1136,7 @@ 'iconv_strpos' => ['hasSideEffects' => false], 'iconv_strrpos' => ['hasSideEffects' => false], 'iconv_substr' => ['hasSideEffects' => false], - 'idate' => ['hasSideEffects' => false], + 'idate' => ['hasSideEffects' => true], 'image_type_to_extension' => ['hasSideEffects' => false], 'image_type_to_mime_type' => ['hasSideEffects' => false], 'imagecolorat' => ['hasSideEffects' => false], @@ -1063,13 +1171,13 @@ 'inflate_get_status' => ['hasSideEffects' => false], 'inflate_init' => ['hasSideEffects' => false], 'ini_get' => ['hasSideEffects' => false], - 'ini_get_all' => ['hasSideEffects' => false], + 'ini_get_all' => ['hasSideEffects' => true], 'intcal_get_maximum' => ['hasSideEffects' => false], 'intdiv' => ['hasSideEffects' => false], 'intl_error_name' => ['hasSideEffects' => false], 'intl_get' => ['hasSideEffects' => false], - 'intl_get_error_code' => ['hasSideEffects' => false], - 'intl_get_error_message' => ['hasSideEffects' => false], + 'intl_get_error_code' => ['hasSideEffects' => true], + 'intl_get_error_message' => ['hasSideEffects' => true], 'intl_is_failure' => ['hasSideEffects' => false], 'intlcal_after' => ['hasSideEffects' => false], 'intlcal_before' => ['hasSideEffects' => false], @@ -1082,8 +1190,8 @@ 'intlcal_get_actual_minimum' => ['hasSideEffects' => false], 'intlcal_get_available_locales' => ['hasSideEffects' => false], 'intlcal_get_day_of_week_type' => ['hasSideEffects' => false], - 'intlcal_get_error_code' => ['hasSideEffects' => false], - 'intlcal_get_error_message' => ['hasSideEffects' => false], + 'intlcal_get_error_code' => ['hasSideEffects' => true], + 'intlcal_get_error_message' => ['hasSideEffects' => true], 'intlcal_get_first_day_of_week' => ['hasSideEffects' => false], 'intlcal_get_greatest_minimum' => ['hasSideEffects' => false], 'intlcal_get_keyword_values_for_locale' => ['hasSideEffects' => false], @@ -1092,7 +1200,7 @@ 'intlcal_get_maximum' => ['hasSideEffects' => false], 'intlcal_get_minimal_days_in_first_week' => ['hasSideEffects' => false], 'intlcal_get_minimum' => ['hasSideEffects' => false], - 'intlcal_get_now' => ['hasSideEffects' => false], + 'intlcal_get_now' => ['hasSideEffects' => true], 'intlcal_get_repeated_wall_time_option' => ['hasSideEffects' => false], 'intlcal_get_skipped_wall_time_option' => ['hasSideEffects' => false], 'intlcal_get_time' => ['hasSideEffects' => false], @@ -1119,8 +1227,8 @@ 'intltz_get_display_name' => ['hasSideEffects' => false], 'intltz_get_dst_savings' => ['hasSideEffects' => false], 'intltz_get_equivalent_id' => ['hasSideEffects' => false], - 'intltz_get_error_code' => ['hasSideEffects' => false], - 'intltz_get_error_message' => ['hasSideEffects' => false], + 'intltz_get_error_code' => ['hasSideEffects' => true], + 'intltz_get_error_message' => ['hasSideEffects' => true], 'intltz_get_gmt' => ['hasSideEffects' => false], 'intltz_get_id' => ['hasSideEffects' => false], 'intltz_get_offset' => ['hasSideEffects' => false], @@ -1162,19 +1270,23 @@ 'is_scalar' => ['hasSideEffects' => false], 'is_string' => ['hasSideEffects' => false], 'is_subclass_of' => ['hasSideEffects' => false], - 'is_uploaded_file' => ['hasSideEffects' => false], + 'is_uploaded_file' => ['hasSideEffects' => true], 'is_writable' => ['hasSideEffects' => false], 'is_writeable' => ['hasSideEffects' => false], 'iterator_count' => ['hasSideEffects' => false], 'join' => ['hasSideEffects' => false], 'json_last_error' => ['hasSideEffects' => false], 'json_last_error_msg' => ['hasSideEffects' => false], + 'json_validate' => ['hasSideEffects' => false], 'key' => ['hasSideEffects' => false], 'key_exists' => ['hasSideEffects' => false], 'lcfirst' => ['hasSideEffects' => false], - 'libxml_get_errors' => ['hasSideEffects' => false], - 'libxml_get_last_error' => ['hasSideEffects' => false], - 'linkinfo' => ['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], @@ -1192,8 +1304,8 @@ 'locale_get_script' => ['hasSideEffects' => false], 'locale_lookup' => ['hasSideEffects' => false], 'locale_parse' => ['hasSideEffects' => false], - 'localeconv' => ['hasSideEffects' => false], - 'localtime' => ['hasSideEffects' => false], + 'localeconv' => ['hasSideEffects' => true], + 'localtime' => ['hasSideEffects' => true], 'log' => ['hasSideEffects' => false], 'log10' => ['hasSideEffects' => false], 'log1p' => ['hasSideEffects' => false], @@ -1229,6 +1341,7 @@ 'mb_preferred_mime_name' => ['hasSideEffects' => false], 'mb_scrub' => ['hasSideEffects' => false], 'mb_split' => ['hasSideEffects' => false], + 'mb_str_pad' => ['hasSideEffects' => false], 'mb_str_split' => ['hasSideEffects' => false], 'mb_strcut' => ['hasSideEffects' => false], 'mb_strimwidth' => ['hasSideEffects' => false], @@ -1248,9 +1361,9 @@ 'mb_substr_count' => ['hasSideEffects' => false], 'mbereg_search_setpos' => ['hasSideEffects' => false], 'md5' => ['hasSideEffects' => false], - 'md5_file' => ['hasSideEffects' => false], - 'memory_get_peak_usage' => ['hasSideEffects' => false], - 'memory_get_usage' => ['hasSideEffects' => false], + 'md5_file' => ['hasSideEffects' => true], + 'memory_get_peak_usage' => ['hasSideEffects' => true], + 'memory_get_usage' => ['hasSideEffects' => true], 'metaphone' => ['hasSideEffects' => false], 'method_exists' => ['hasSideEffects' => false], 'mhash' => ['hasSideEffects' => false], @@ -1258,14 +1371,16 @@ 'mhash_get_block_size' => ['hasSideEffects' => false], 'mhash_get_hash_name' => ['hasSideEffects' => false], 'mhash_keygen_s2k' => ['hasSideEffects' => false], - 'microtime' => ['hasSideEffects' => false], + 'microtime' => ['hasSideEffects' => true], 'min' => ['hasSideEffects' => false], - 'mktime' => ['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' => false], - 'msgfmt_get_error_message' => ['hasSideEffects' => false], + 'msgfmt_get_error_code' => ['hasSideEffects' => true], + 'msgfmt_get_error_message' => ['hasSideEffects' => true], 'msgfmt_get_locale' => ['hasSideEffects' => false], 'msgfmt_get_pattern' => ['hasSideEffects' => false], 'msgfmt_parse' => ['hasSideEffects' => false], @@ -1275,7 +1390,7 @@ 'net_get_interfaces' => ['hasSideEffects' => false], 'ngettext' => ['hasSideEffects' => false], 'nl2br' => ['hasSideEffects' => false], - 'nl_langinfo' => ['hasSideEffects' => false], + 'nl_langinfo' => ['hasSideEffects' => true], 'normalizer_get_raw_decomposition' => ['hasSideEffects' => false], 'normalizer_is_normalized' => ['hasSideEffects' => false], 'normalizer_normalize' => ['hasSideEffects' => false], @@ -1284,23 +1399,39 @@ 'numfmt_format' => ['hasSideEffects' => false], 'numfmt_format_currency' => ['hasSideEffects' => false], 'numfmt_get_attribute' => ['hasSideEffects' => false], - 'numfmt_get_error_code' => ['hasSideEffects' => false], - 'numfmt_get_error_message' => ['hasSideEffects' => false], + 'numfmt_get_error_code' => ['hasSideEffects' => true], + 'numfmt_get_error_message' => ['hasSideEffects' => true], 'numfmt_get_locale' => ['hasSideEffects' => false], 'numfmt_get_pattern' => ['hasSideEffects' => false], 'numfmt_get_symbol' => ['hasSideEffects' => false], 'numfmt_get_text_attribute' => ['hasSideEffects' => false], 'numfmt_parse' => ['hasSideEffects' => false], + 'ob_clean' => ['hasSideEffects' => true], + 'ob_end_clean' => ['hasSideEffects' => true], + 'ob_end_flush' => ['hasSideEffects' => true], 'ob_etaghandler' => ['hasSideEffects' => false], + 'ob_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_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' => false], - 'pcntl_errno' => ['hasSideEffects' => false], - 'pcntl_get_last_error' => ['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], @@ -1315,15 +1446,16 @@ 'php_ini_scanned_files' => ['hasSideEffects' => false], 'php_logo_guid' => ['hasSideEffects' => false], 'php_sapi_name' => ['hasSideEffects' => false], - 'php_strip_whitespace' => ['hasSideEffects' => false], - 'php_uname' => ['hasSideEffects' => false], + 'php_strip_whitespace' => ['hasSideEffects' => true], + 'php_uname' => ['hasSideEffects' => true], 'phpversion' => ['hasSideEffects' => false], 'pi' => ['hasSideEffects' => false], + 'popen' => ['hasSideEffects' => true], 'pos' => ['hasSideEffects' => false], 'posix_ctermid' => ['hasSideEffects' => false], - 'posix_errno' => ['hasSideEffects' => false], - 'posix_get_last_error' => ['hasSideEffects' => false], - 'posix_getcwd' => ['hasSideEffects' => false], + 'posix_errno' => ['hasSideEffects' => true], + 'posix_get_last_error' => ['hasSideEffects' => true], + 'posix_getcwd' => ['hasSideEffects' => true], 'posix_getegid' => ['hasSideEffects' => false], 'posix_geteuid' => ['hasSideEffects' => false], 'posix_getgid' => ['hasSideEffects' => false], @@ -1348,8 +1480,8 @@ 'posix_uname' => ['hasSideEffects' => false], 'pow' => ['hasSideEffects' => false], 'preg_grep' => ['hasSideEffects' => false], - 'preg_last_error' => ['hasSideEffects' => false], - 'preg_last_error_msg' => ['hasSideEffects' => false], + 'preg_last_error' => ['hasSideEffects' => true], + 'preg_last_error_msg' => ['hasSideEffects' => true], 'preg_quote' => ['hasSideEffects' => false], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], @@ -1363,19 +1495,24 @@ 'range' => ['hasSideEffects' => false], 'rawurldecode' => ['hasSideEffects' => false], 'rawurlencode' => ['hasSideEffects' => false], - 'realpath' => ['hasSideEffects' => false], - 'realpath_cache_get' => ['hasSideEffects' => false], - 'realpath_cache_size' => ['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' => false], - 'resourcebundle_get_error_message' => ['hasSideEffects' => false], + 'resourcebundle_get_error_code' => ['hasSideEffects' => true], + 'resourcebundle_get_error_message' => ['hasSideEffects' => true], 'resourcebundle_locales' => ['hasSideEffects' => false], + 'rewind' => ['hasSideEffects' => true], + 'rmdir' => ['hasSideEffects' => true], 'round' => ['hasSideEffects' => false], 'rtrim' => ['hasSideEffects' => false], 'sha1' => ['hasSideEffects' => false], - 'sha1_file' => ['hasSideEffects' => false], + 'sha1_file' => ['hasSideEffects' => true], 'sin' => ['hasSideEffects' => false], 'sinh' => ['hasSideEffects' => false], 'sizeof' => ['hasSideEffects' => false], @@ -1386,12 +1523,13 @@ '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_shuffle' => ['hasSideEffects' => false], 'str_split' => ['hasSideEffects' => false], 'str_starts_with' => ['hasSideEffects' => false], 'str_word_count' => ['hasSideEffects' => false], @@ -1400,9 +1538,9 @@ 'strcmp' => ['hasSideEffects' => false], 'strcoll' => ['hasSideEffects' => false], 'strcspn' => ['hasSideEffects' => false], - 'stream_get_filters' => ['hasSideEffects' => false], - 'stream_get_transports' => ['hasSideEffects' => false], - 'stream_get_wrappers' => ['hasSideEffects' => false], + 'stream_get_filters' => ['hasSideEffects' => true], + 'stream_get_transports' => ['hasSideEffects' => true], + 'stream_get_wrappers' => ['hasSideEffects' => true], 'stream_is_local' => ['hasSideEffects' => false], 'stream_isatty' => ['hasSideEffects' => false], 'strip_tags' => ['hasSideEffects' => false], @@ -1417,7 +1555,7 @@ 'strncmp' => ['hasSideEffects' => false], 'strpbrk' => ['hasSideEffects' => false], 'strpos' => ['hasSideEffects' => false], - 'strptime' => ['hasSideEffects' => false], + 'strptime' => ['hasSideEffects' => true], 'strrchr' => ['hasSideEffects' => false], 'strrev' => ['hasSideEffects' => false], 'strripos' => ['hasSideEffects' => false], @@ -1425,7 +1563,7 @@ 'strspn' => ['hasSideEffects' => false], 'strstr' => ['hasSideEffects' => false], 'strtolower' => ['hasSideEffects' => false], - 'strtotime' => ['hasSideEffects' => false], + 'strtotime' => ['hasSideEffects' => true], 'strtoupper' => ['hasSideEffects' => false], 'strtr' => ['hasSideEffects' => false], 'strval' => ['hasSideEffects' => false], @@ -1433,36 +1571,43 @@ 'substr_compare' => ['hasSideEffects' => false], 'substr_count' => ['hasSideEffects' => false], 'substr_replace' => ['hasSideEffects' => false], - 'sys_getloadavg' => ['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' => false], - 'timezone_location_get' => ['hasSideEffects' => false], - 'timezone_name_from_abbr' => ['hasSideEffects' => false], + 'timezone_identifiers_list' => ['hasSideEffects' => true], + 'timezone_location_get' => ['hasSideEffects' => true], + 'timezone_name_from_abbr' => ['hasSideEffects' => true], 'timezone_name_get' => ['hasSideEffects' => false], - 'timezone_offset_get' => ['hasSideEffects' => false], - 'timezone_open' => ['hasSideEffects' => false], - 'timezone_transitions_get' => ['hasSideEffects' => false], + 'timezone_offset_get' => ['hasSideEffects' => true], + 'timezone_open' => ['hasSideEffects' => true], + 'timezone_transitions_get' => ['hasSideEffects' => true], 'timezone_version_get' => ['hasSideEffects' => false], + 'tmpfile' => ['hasSideEffects' => true], 'token_get_all' => ['hasSideEffects' => false], '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' => false], - 'transliterator_get_error_message' => ['hasSideEffects' => false], + 'transliterator_get_error_code' => ['hasSideEffects' => true], + 'transliterator_get_error_message' => ['hasSideEffects' => true], 'transliterator_list_ids' => ['hasSideEffects' => false], 'transliterator_transliterate' => ['hasSideEffects' => false], 'trim' => ['hasSideEffects' => false], 'ucfirst' => ['hasSideEffects' => false], 'ucwords' => ['hasSideEffects' => false], - 'uniqid' => ['hasSideEffects' => false], + 'umask' => ['hasSideEffects' => true], + 'uniqid' => ['hasSideEffects' => true], + 'unlink' => ['hasSideEffects' => true], 'unpack' => ['hasSideEffects' => false], 'urldecode' => ['hasSideEffects' => false], 'urlencode' => ['hasSideEffects' => false], 'utf8_decode' => ['hasSideEffects' => false], 'utf8_encode' => ['hasSideEffects' => false], + 'version_compare' => ['hasSideEffects' => false], 'vsprintf' => ['hasSideEffects' => false], 'wordwrap' => ['hasSideEffects' => false], 'xml_error_string' => ['hasSideEffects' => false], diff --git a/src/AnalysedCodeException.php b/src/AnalysedCodeException.php index d4e7558622..6d78350e50 100644 --- a/src/AnalysedCodeException.php +++ b/src/AnalysedCodeException.php @@ -2,7 +2,9 @@ namespace PHPStan; -abstract class AnalysedCodeException extends \Exception +use Exception; + +abstract class AnalysedCodeException extends Exception { abstract public function getTip(): ?string; diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 6941c4a0d6..e8f239ba84 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -2,49 +2,48 @@ namespace PHPStan\Analyser; -use PHPStan\Rules\Registry; - -class Analyser +use Closure; +use PHPStan\Collectors\CollectedData; +use PHPStan\Collectors\Registry as CollectorRegistry; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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 + */ +#[AutowiredService] +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, + #[AutowiredParameter] + 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 +53,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 = []; + $usedTraitDependencies = []; $exportedNodes = []; foreach ($files as $file) { if ($preFileCallback !== null) { @@ -70,29 +83,36 @@ 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(); + $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); $fileExportedNodes = $fileAnalyserResult->getExportedNodes(); if (count($fileExportedNodes) > 0) { $exportedNodes[$file] = $fileExportedNodes; } - } catch (\Throwable $t) { + } 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?template=Bug_report.md' - ); - $errors[] = new Error($internalErrorMessage, $file, null, $t); + $errors[] = (new Error($t->getMessage(), $file, canBeIgnored: $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; @@ -106,44 +126,21 @@ 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, + $internalErrorsCount === 0 ? $usedTraitDependencies : null, $exportedNodes, - $reachedInternalErrorsCountLimit + $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 d045f84749..576471d1ff 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -2,49 +2,69 @@ namespace PHPStan\Analyser; -use PHPStan\Dependency\ExportedNode; - -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; - - /** @var array> */ - private array $exportedNodes; - - 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 array> $exportedNodes - * @param bool $reachedInternalErrorsCountLimit + * @param array>|null $usedTraitDependencies + * @param array> $exportedNodes */ public function __construct( - array $errors, - array $internalErrors, - ?array $dependencies, - array $exportedNodes, - 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 $usedTraitDependencies, + private array $exportedNodes, + private bool $reachedInternalErrorsCountLimit, + private int $peakMemoryUsageBytes, ) { - $this->unorderedErrors = $errors; + } + + /** + * @return list + */ + public function getUnorderedErrors(): array + { + return $this->unorderedErrors; + } - usort( - $errors, - static function (Error $a, Error $b): int { - return [ + /** + * @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(), @@ -52,41 +72,69 @@ static function (Error $a, Error $b): int { $b->getFile(), $b->getLine(), $b->getMessage(), - ]; - } - ); - - $this->errors = $errors; - $this->internalErrors = $internalErrors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; - $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 string[] + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return list */ public function getInternalErrors(): array { return $this->internalErrors; } + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + /** * @return array>|null */ @@ -96,7 +144,15 @@ public function getDependencies(): ?array } /** - * @return array> + * @return array>|null + */ + public function getUsedTraitDependencies(): ?array + { + return $this->usedTraitDependencies; + } + + /** + * @return array> */ public function getExportedNodes(): array { @@ -108,4 +164,9 @@ public function hasReachedInternalErrorsCountLimit(): bool return $this->reachedInternalErrorsCountLimit; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + } diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php new file mode 100644 index 0000000000..97c3bdfd1c --- /dev/null +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -0,0 +1,240 @@ +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, tip: $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, tip: '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, [], $node); + + 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->getUsedTraitDependencies(), + $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->getUsedTraitDependencies(), + $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->getUsedTraitDependencies(), + $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..4a4844cd8c --- /dev/null +++ b/src/Analyser/ArgumentsNormalizer.php @@ -0,0 +1,328 @@ +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 identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $functionCall->getArgs()) { + return $functionCall; + } + + 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 identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $methodCall->getArgs()) { + return $methodCall; + } + + 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 identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $staticCall->getArgs()) { + return $staticCall; + } + + 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 identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $new->getArgs()) { + return $new; + } + + 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 + + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $reorderedArgs[$i] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) { + $argName = $arg->name->toString(); + // order named args into the position the signature expects them + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $reorderedArgs[$argumentPositions[$argName]] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + } else { + if (!$hasVariadic) { + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $appendArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + continue; + } + + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $additionalNamedArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + } + } + + // replace variadic parameter with additional named args, except if it is already set + $additionalNamedArgsOffset = count($argumentPositions) - 1; + if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) { + $additionalNamedArgsOffset++; + } + + foreach ($additionalNamedArgs as $i => $additionalNamedArg) { + $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg; + } + + if (count($reorderedArgs) === 0) { + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; + } + + // fill up all holes with default values until the last given argument + for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) { + if (array_key_exists($j, $reorderedArgs)) { + continue; + } + if (!array_key_exists($j, $signatureParameters)) { + throw new ShouldNotHappenException('Parameter signatures cannot have holes'); + } + + $parameter = $signatureParameters[$j]; + + // we can only fill up optional parameters with default values + if (!$parameter->isOptional()) { + return null; + } + + $defaultValue = $parameter->getDefaultValue(); + if ($defaultValue === null) { + if (!$parameter->isVariadic()) { + throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName())); + } + $defaultValue = new ConstantArrayType([], []); + } + + $reorderedArgs[$j] = new Arg( + new TypeExpr($defaultValue), + ); + } + + ksort($reorderedArgs); + + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + + return $reorderedArgs; + } + +} diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index 834dd49d03..6907183966 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -2,42 +2,37 @@ namespace PHPStan\Analyser; -use PHPStan\Type\Type; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; +use function count; +use function implode; +use function sprintf; -class ConditionalExpressionHolder +final class ConditionalExpressionHolder { - /** @var array */ - private array $conditionExpressionTypes; - - private VariableTypeHolder $typeHolder; - /** - * @param array $conditionExpressionTypes - * @param VariableTypeHolder $typeHolder + * @param array $conditionExpressionTypeHolders */ public function __construct( - array $conditionExpressionTypes, - VariableTypeHolder $typeHolder + private array $conditionExpressionTypeHolders, + private ExpressionTypeHolder $typeHolder, ) { - if (count($conditionExpressionTypes) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + if (count($conditionExpressionTypeHolders) === 0) { + throw new ShouldNotHappenException(); } - $this->conditionExpressionTypes = $conditionExpressionTypes; - $this->typeHolder = $typeHolder; } /** - * @return array + * @return array */ - public function getConditionExpressionTypes(): array + public function getConditionExpressionTypeHolders(): array { - return $this->conditionExpressionTypes; + return $this->conditionExpressionTypeHolders; } - public function getTypeHolder(): VariableTypeHolder + public function getTypeHolder(): ExpressionTypeHolder { return $this->typeHolder; } @@ -45,15 +40,15 @@ public function getTypeHolder(): VariableTypeHolder public function getKey(): string { $parts = []; - foreach ($this->conditionExpressionTypes as $exprString => $type) { - $parts[] = $exprString . '=' . $type->describe(VerbosityLevel::precise()); + foreach ($this->conditionExpressionTypeHolders as $exprString => $typeHolder) { + $parts[] = $exprString . '=' . $typeHolder->getType()->describe(VerbosityLevel::precise()); } return sprintf( '%s => %s (%s)', implode(' && ', $parts), $this->typeHolder->getType()->describe(VerbosityLevel::precise()), - $this->typeHolder->getCertainty()->describe() + $this->typeHolder->getCertainty()->describe(), ); } diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php new file mode 100644 index 0000000000..d7e8001de6 --- /dev/null +++ b/src/Analyser/ConstantResolver.php @@ -0,0 +1,471 @@ + */ + 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, + private ?Container $container, + ) + { + } + + 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 = self::PHP_MIN_ANALYZABLE_VERSION_ID; + $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()) { + if (array_key_exists($constantName, $this->dynamicConstantNames)) { + $phpdocTypes = $this->dynamicConstantNames[$constantName]; + if ($this->container !== null) { + $typeStringResolver = $this->container->getByType(TypeStringResolver::class); + return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], null)); + } + return $constantType; + } + if (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 (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { + if ($constantType->isConstantValue()->yes()) { + $phpdocTypes = $this->dynamicConstantNames[$lookupConstantName]; + if ($this->container !== null) { + $typeStringResolver = $this->container->getByType(TypeStringResolver::class); + return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className)); + } + } + + if ($nativeType !== null) { + return $nativeType; + } + return $constantType; + } + + 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..eae83a5ac6 --- /dev/null +++ b/src/Analyser/ConstantResolverFactory.php @@ -0,0 +1,34 @@ +container->getByType(ComposerPhpVersionFactory::class); + + return new ConstantResolver( + $this->reflectionProviderProvider, + $this->container->getParameter('dynamicConstantNames'), + $this->container->getParameter('phpVersion'), + $composerFactory, + $this->container, + ); + } + +} 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 eb7767275e..0000000000 --- a/src/Analyser/DirectScopeFactory.php +++ /dev/null @@ -1,144 +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->nodeScopeResolver = $nodeScopeResolver; - $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 array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return new $scopeClass( - $this, - $this->reflectionProvider, - $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), - $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry(), - $this->printer, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->nodeScopeResolver, - $context, - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $conditionalExpressions, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain, - $afterExtractCall, - $parentScope - ); - } - -} 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 adc3ebfb22..59f5eba2ee 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -3,26 +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; - - private Type $originalNativeType; - public function __construct( - Expr $expression, - Type $originalType, - Type $originalNativeType + private Expr $expression, + private Type $originalType, + private Type $originalNativeType, + private TrinaryLogic $certainty, ) { - $this->expression = $expression; - $this->originalType = $originalType; - $this->originalNativeType = $originalNativeType; } public function getExpression(): Expr @@ -40,4 +33,9 @@ public function getOriginalNativeType(): Type return $this->originalNativeType; } + public function getCertainty(): TrinaryLogic + { + return $this->certainty; + } + } diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 7a80edbf8b..9af5f2b297 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -2,74 +2,49 @@ namespace PHPStan\Analyser; -class Error implements \JsonSerializable +use Exception; +use JsonSerializable; +use Nette\Utils\Strings; +use Override; +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; - - /** @var bool|\Throwable */ - private $canBeIgnored; - - private ?string $filePath; - - private ?string $traitFilePath; - - private ?string $tip; - - private ?int $nodeLine; - - /** @phpstan-var class-string<\PhpParser\Node>|null */ - private ?string $nodeType; - - private ?string $identifier; - - /** @var mixed[] */ - private array $metadata; + 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|\Throwable $canBeIgnored - * @param string|null $filePath - * @param string|null $traitFilePath - * @param string|null $tip - * @param int|null $nodeLine - * @param class-string<\PhpParser\Node>|null $nodeType - * @param string|null $identifier + * @param class-string|null $nodeType * @param mixed[] $metadata */ public function __construct( - string $message, - string $file, - ?int $line = null, - $canBeIgnored = true, - ?string $filePath = null, - ?string $traitFilePath = null, - ?string $tip = null, - ?int $nodeLine = null, - ?string $nodeType = null, - ?string $identifier = null, - array $metadata = [] + private string $message, + private string $file, + private ?int $line = null, + private bool|Throwable $canBeIgnored = true, + private ?string $filePath = null, + private ?string $traitFilePath = null, + private ?string $tip = null, + private ?int $nodeLine = null, + private ?string $nodeType = null, + private ?string $identifier = null, + private array $metadata = [], + private ?FixedErrorDiff $fixedErrorDiff = null, ) { - $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 @@ -94,7 +69,7 @@ public function getFilePath(): string public function changeFilePath(string $newFilePath): self { if ($this->traitFilePath !== null) { - throw new \PHPStan\ShouldNotHappenException('Errors in traits not yet supported'); + throw new ShouldNotHappenException('Errors in traits not yet supported'); } return new self( @@ -108,7 +83,8 @@ public function changeFilePath(string $newFilePath): self $this->nodeLine, $this->nodeType, $this->identifier, - $this->metadata + $this->metadata, + $this->fixedErrorDiff, ); } @@ -125,7 +101,8 @@ public function changeTraitFilePath(string $newFilePath): self $this->nodeLine, $this->nodeType, $this->identifier, - $this->metadata + $this->metadata, + $this->fixedErrorDiff, ); } @@ -146,7 +123,7 @@ public function canBeIgnored(): bool public function hasNonIgnorableException(): bool { - return $this->canBeIgnored instanceof \Throwable; + return $this->canBeIgnored instanceof Throwable; } public function getTip(): ?string @@ -169,7 +146,79 @@ public function withoutTip(): self $this->traitFilePath, null, $this->nodeLine, - $this->nodeType + $this->nodeType, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, + ); + } + + 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, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, + ); + } + + 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, + $this->fixedErrorDiff, + ); + } + + /** + * @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, + $this->fixedErrorDiff, ); } @@ -179,13 +228,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; @@ -199,11 +253,28 @@ public function getMetadata(): array return $this->metadata; } + /** + * @internal Experimental + */ + public function getFixedErrorDiff(): ?FixedErrorDiff + { + return $this->fixedErrorDiff; + } + /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { + $fixedErrorDiffHash = null; + $fixedErrorDiffDiff = null; + if ($this->fixedErrorDiff !== null) { + $fixedErrorDiffHash = $this->fixedErrorDiff->originalHash; + $fixedErrorDiffDiff = $this->fixedErrorDiff->diff; + } + return [ 'message' => $this->message, 'file' => $this->file, @@ -216,33 +287,39 @@ public function jsonSerialize() 'nodeType' => $this->nodeType, 'identifier' => $this->identifier, 'metadata' => $this->metadata, + 'fixedErrorDiffHash' => $fixedErrorDiffHash, + 'fixedErrorDiffDiff' => $fixedErrorDiffDiff, ]; } /** * @param mixed[] $json - * @return self */ public static function decode(array $json): self { + $fixedErrorDiff = null; + if ($json['fixedErrorDiffHash'] !== null && $json['fixedErrorDiffDiff'] !== null) { + $fixedErrorDiff = new FixedErrorDiff($json['fixedErrorDiffHash'], $json['fixedErrorDiffDiff']); + } + return new self( $json['message'], $json['file'], $json['line'], - $json['canBeIgnored'] === 'exception' ? new \Exception() : $json['canBeIgnored'], + $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'], $json['filePath'], $json['traitFilePath'], $json['tip'], $json['nodeLine'] ?? null, $json['nodeType'] ?? null, $json['identifier'] ?? null, - $json['metadata'] ?? [] + $json['metadata'] ?? [], + $fixedErrorDiff, ); } /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -257,8 +334,14 @@ public static function __set_state(array $properties): self $properties['nodeLine'] ?? null, $properties['nodeType'] ?? null, $properties['identifier'] ?? null, - $properties['metadata'] ?? [] + $properties['metadata'] ?? [], + $properties['fixedErrorDiff'] ?? null, ); } + 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 4cec309c24..4b586b812b 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,16 +2,9 @@ namespace PHPStan\Analyser; -class ExpressionResult +final class ExpressionResult { - private MutatingScope $scope; - - private bool $hasYield; - - /** @var ThrowPoint[] $throwPoints */ - private array $throwPoints; - /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -23,23 +16,21 @@ 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, - array $throwPoints, + private MutatingScope $scope, + private bool $hasYield, + private bool $isAlwaysTerminating, + private array $throwPoints, + private array $impurePoints, ?callable $truthyScopeCallback = null, - ?callable $falseyScopeCallback = null + ?callable $falseyScopeCallback = null, ) { - $this->scope = $scope; - $this->hasYield = $hasYield; - $this->throwPoints = $throwPoints; $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; } @@ -62,6 +53,14 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getTruthyScope(): MutatingScope { if ($this->truthyScopeCallback === null) { @@ -92,4 +91,9 @@ public function getFalseyScope(): MutatingScope return $this->falseyScope; } + public function isAlwaysTerminating(): bool + { + return $this->isAlwaysTerminating; + } + } 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 c13f5429b3..fbdd71d83f 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -2,169 +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\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; +use PHPStan\BetterReflection\Reflection\Exception\CircularReference; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\Collectors\CollectedData; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FileNode; +use PHPStan\Node\InClassNode; +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 function array_key_exists; +use PHPStan\Parser\ParserErrorsException; +use PHPStan\Rules\Registry as RuleRegistry; +use function array_keys; use function array_unique; +use function array_values; +use function count; +use function error_reporting; +use function get_class; +use function is_dir; +use function is_file; +use function 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; -class FileAnalyser +/** + * @phpstan-import-type CollectorData from CollectedData + */ +#[AutowiredService] +final class FileAnalyser { - private \PHPStan\Analyser\ScopeFactory $scopeFactory; + /** @var list */ + private array $allPhpErrors = []; - private \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Parser\Parser $parser; - - private DependencyResolver $dependencyResolver; - - private bool $reportUnmatchedIgnoredErrors; + /** @var list */ + private array $filteredPhpErrors = []; public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - Parser $parser, - DependencyResolver $dependencyResolver, - bool $reportUnmatchedIgnoredErrors + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + 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->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 = []; + $usedTraitFileDependencies = []; $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, &$exportedNodes, $file, $registry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$temporaryFileErrors): void { + $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$usedTraitFileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$temporaryFileErrors, $parserNodes): 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 ($scope->isInTrait()) { + $traitReflection = $scope->getTraitReflection(); + if ($traitReflection->getFileName() !== null) { + $traitFilePath = $traitReflection->getFileName(); + $parserNodes = $this->parser->parseFile($traitFilePath); + } + } + 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(), $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, tip: $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, tip: 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } foreach ($ruleErrors as $ruleError) { - $nodeLine = $node->getLine(); - $line = $nodeLine; - $canBeIgnored = true; - $fileName = $scope->getFileDescription(); - $filePath = $scope->getFile(); - $traitFilePath = null; - $tip = null; - $identifier = null; - $metadata = []; - if ($scope->isInTrait()) { - $traitReflection = $scope->getTraitReflection(); - if ($traitReflection->getFileName() !== null) { - $traitFilePath = $traitReflection->getFileName(); + $error = $this->ruleErrorTransformer->transform($ruleError, $scope, $parserNodes, $node); + + 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, tip: $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, tip: '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[$scope->getFileDescription()][$lineToIgnore] = true; + $fileCollectedData[$scope->getFile()][get_class($collector)][] = $collectedData; } try { @@ -175,13 +224,22 @@ public function analyseFile( if ($dependencies->getExportedNode() !== null) { $exportedNodes[] = $dependencies->getExportedNode(); } - } catch (\PHPStan\AnalysedCodeException $e) { + } catch (AnalysedCodeException) { // pass - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { + } catch (UnableToCompileNode) { // pass } + + if (!$node instanceof InClassNode) { + return; + } + + $usedTraitDependencies = $this->dependencyResolver->resolveUsedTraitDependencies($node); + foreach ($usedTraitDependencies->getFileDependencies($scope->getFile(), $analysedFiles) as $dependentFile) { + $usedTraitFileDependencies[] = $dependentFile; + } }; $scope = $this->scopeFactory->create(ScopeContext::create($file)); @@ -189,115 +247,164 @@ public function analyseFile( $this->nodeScopeResolver->processNodes( $parserNodes, $scope, - $nodeCallback + $nodeCallback, ); - $unmatchedLineIgnores = $linesToIgnore; - foreach ($temporaryFileErrors as $tmpFileError) { - $line = $tmpFileError->getLine(); - if ( - $line !== null - && $tmpFileError->canBeIgnored() - && array_key_exists($tmpFileError->getFile(), $linesToIgnore) - && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) - ) { - unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); - continue; - } - $fileErrors[] = $tmpFileError; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + $temporaryFileErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $fileError) { + $fileErrors[] = $fileError; } - - if ($this->reportUnmatchedIgnoredErrors) { - foreach ($unmatchedLineIgnores as $ignoredFile => $lines) { - if ($ignoredFile !== $file) { - continue; - } - - foreach (array_keys($lines) as $line) { - $fileErrors[] = new Error( - sprintf('No error to ignore is reported on line %d.', $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - null, - null, - null, - null, - 'ignoredError.unmatchedOnLine' - ); - } - } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; } + $linesToIgnore = $localIgnoresProcessorResult->getLinesToIgnore(); + $unmatchedLineIgnores = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); } catch (\PhpParser\Error $e) { - $fileErrors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e); - } 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(), $e->getParsedFile() ?? $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, $e); + $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } - } catch (\PHPStan\AnalysedCodeException $e) { - $fileErrors[] = new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()); + } catch (AnalysedCodeException $e) { + $fileErrors[] = (new Error($e->getMessage(), $file, canBeIgnored: $e, tip: $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, canBeIgnored: $e, tip: '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, canBeIgnored: $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + } finally { + $this->restoreCollectErrorsHandler(); } } 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, canBeIgnored: 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, canBeIgnored: false))->withIdentifier('phpstan.path'); + } + + foreach ($linesToIgnore as $fileKey => $lines) { + if (count($lines) > 0) { + continue; + } + + unset($linesToIgnore[$fileKey]); + } + + foreach ($unmatchedLineIgnores as $fileKey => $lines) { + if (count($lines) > 0) { + continue; + } + + unset($unmatchedLineIgnores[$fileKey]); } - return new FileAnalyserResult($fileErrors, array_values(array_unique($fileDependencies)), $exportedNodes); + return new FileAnalyserResult( + $fileErrors, + $this->filteredPhpErrors, + $this->allPhpErrors, + $locallyIgnoredErrors, + $fileCollectedData, + array_values(array_unique($fileDependencies)), + array_values(array_unique($usedTraitFileDependencies)), + $exportedNodes, + $linesToIgnore, + $unmatchedLineIgnores, + ); } /** - * @param Node $node - * @return int[] + * @param Node[] $nodes + * @return array|null> */ - private function getLinesToIgnore(Node $node): array + private function getLinesToIgnoreFromTokens(array $nodes): array { - $lines = []; - if ($node->getDocComment() !== null) { - $line = $this->findLineToIgnoreComment($node->getDocComment()); - if ($line !== null) { - $lines[] = $line; - } + if (!isset($nodes[0])) { + return []; } - foreach ($node->getComments() as $comment) { - $line = $this->findLineToIgnoreComment($comment); - if ($line === null) { - continue; + /** @var array|null> */ + return $nodes[0]->getAttribute('linesToIgnore', []); + } + + /** + * @param array $analysedFiles + */ + private function collectErrors(array $analysedFiles): void + { + $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; } - $lines[] = $line; - } + $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[$errfile])) { + return true; + } - return $lines; + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, $errno === E_USER_DEPRECATED))->withIdentifier('phpstan.php'); + + return true; + }); } - private function findLineToIgnoreComment(Comment $comment): ?int + private function restoreCollectErrorsHandler(): void { - $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; - } + restore_error_handler(); + } - if (strpos($text, '@phpstan-ignore-line') !== false) { - return $line; + 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 null; + return 'Unknown PHP error'; } } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 024a953026..0d3140f6f8 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -2,42 +2,85 @@ namespace PHPStan\Analyser; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; -class FileAnalyserResult +/** + * @phpstan-type LinesToIgnore = array|null>> + * @phpstan-import-type CollectorData from CollectedData + */ +final class FileAnalyserResult { - /** @var Error[] */ - private array $errors; + /** + * @param list $errors + * @param list $filteredPhpErrors + * @param list $allPhpErrors + * @param list $locallyIgnoredErrors + * @param CollectorData $collectedData + * @param list $dependencies + * @param list $usedTraitDependencies + * @param list $exportedNodes + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function __construct( + private array $errors, + private array $filteredPhpErrors, + private array $allPhpErrors, + private array $locallyIgnoredErrors, + private array $collectedData, + private array $dependencies, + private array $usedTraitDependencies, + private array $exportedNodes, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + ) + { + } - /** @var array */ - private array $dependencies; + /** + * @return list + */ + public function getErrors(): array + { + return $this->errors; + } - /** @var array */ - private array $exportedNodes; + /** + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } /** - * @param Error[] $errors - * @param array $dependencies - * @param array $exportedNodes + * @return list */ - public function __construct(array $errors, array $dependencies, array $exportedNodes) + public function getAllPhpErrors(): array { - $this->errors = $errors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; + return $this->allPhpErrors; } /** - * @return Error[] + * @return list */ - public function getErrors(): array + public function getLocallyIgnoredErrors(): array { - return $this->errors; + return $this->locallyIgnoredErrors; + } + + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; } /** - * @return array + * @return list */ public function getDependencies(): array { @@ -45,11 +88,35 @@ public function getDependencies(): array } /** - * @return array + * @return list + */ + public function getUsedTraitDependencies(): array + { + return $this->usedTraitDependencies; + } + + /** + * @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/FixedErrorDiff.php b/src/Analyser/FixedErrorDiff.php new file mode 100644 index 0000000000..af8755b2f8 --- /dev/null +++ b/src/Analyser/FixedErrorDiff.php @@ -0,0 +1,23 @@ + '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 + { + $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..6aae1fba4c --- /dev/null +++ b/src/Analyser/Ignore/IgnoredError.php @@ -0,0 +1,116 @@ +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 ($ignoredErrorMessage !== null) { + if ($error->getMessage() !== $ignoredErrorMessage) { + 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..19e9428f26 --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -0,0 +1,150 @@ +ignoreErrors as $ignoreError) { + if (is_array($ignoreError)) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['rawMessage']) && !isset($ignoreError['identifier']) && !isset($ignoreError['identifiers'])) { + $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; + } + } elseif (isset($ignoreError['identifiers'])) { + foreach ($ignoreError['identifiers'] as $identifier) { + $expandedIgnoreError = $ignoreError; + unset($expandedIgnoreError['identifiers']); + $expandedIgnoreError['identifier'] = $identifier; + $expandedIgnoreErrors[] = $expandedIgnoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } + + $uniquedExpandedIgnoreErrors = []; + foreach ($expandedIgnoreErrors as $ignoreError) { + if (!isset($ignoreError['message']) && !isset($ignoreError['rawMessage']) && !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['rawMessage'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['rawMessage']); + } + 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, + 'rawMessage' => $ignoreError['rawMessage'] ?? 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['rawMessage']) && !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..7ef13fa85b --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -0,0 +1,245 @@ + $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, null); + if ($shouldBeIgnored) { + unset($unmatchedIgnoredErrors[$i]); + } + } else { + if (isset($ignore['path'])) { + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['rawMessage'] ?? 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['rawMessage'] ?? 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['rawMessage'] ?? 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; + } + + if ($onlyFiles) { + continue; + } + + $errors[] = (new Error( + sprintf( + 'Ignored error pattern %s was not matched in reported errors.', + IgnoredError::stringifyPattern($unmatchedIgnoredError), + ), + $unmatchedIgnoredError['realPath'], + canBeIgnored: 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 f9159dd61e..0000000000 --- a/src/Analyser/IgnoredErrorHelper.php +++ /dev/null @@ -1,156 +0,0 @@ -ignoredRegexValidator = $ignoredRegexValidator; - $this->fileHelper = $fileHelper; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; - } - - public function initialize(): IgnoredErrorHelperResult - { - $otherIgnoreErrors = []; - $ignoreErrorsByFile = []; - $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) { - $errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $errors[] = $this->createAnchorInTheMiddleError($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence()); - } - } 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) { - $errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $errors[] = $this->createAnchorInTheMiddleError($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence()); - } - } - } catch (\Nette\Utils\RegexpException $e) { - $errors[] = $e->getMessage(); - } catch (\Nette\Utils\JsonException $e) { - $errors[] = $e->getMessage(); - } - } - - return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); - } - - /** - * @param string $regex - * @param array $ignoredTypes - * @return string - */ - 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 function (string $typeDescription): string { - return 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/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/IgnoredErrorHelperResult.php deleted file mode 100644 index df9eaa2ff2..0000000000 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ /dev/null @@ -1,246 +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 array> $otherIgnoreErrors - * @param array>> $ignoreErrorsByFile - * @param (string|mixed[])[] $ignoreErrors - * @param bool $reportUnmatchedIgnoredErrors - */ - public function __construct( - FileHelper $fileHelper, - array $errors, - array $otherIgnoreErrors, - array $ignoreErrorsByFile, - array $ignoreErrors, - bool $reportUnmatchedIgnoredErrors - ) - { - $this->fileHelper = $fileHelper; - $this->errors = $errors; - $this->otherIgnoreErrors = $otherIgnoreErrors; - $this->ignoreErrorsByFile = $ignoreErrorsByFile; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; - } - - /** - * @return string[] - */ - public function getErrors(): array - { - return $this->errors; - } - - /** - * @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 excludePaths 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..6aaad7240d --- /dev/null +++ b/src/Analyser/InternalError.php @@ -0,0 +1,106 @@ + + */ +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] + #[Override] + 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..91d8c09a98 --- /dev/null +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -0,0 +1,86 @@ +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 a85269526d..0000000000 --- a/src/Analyser/LazyScopeFactory.php +++ /dev/null @@ -1,112 +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 array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \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->getService('currentPhpVersionSimpleParser'), - $this->container->getByType(NodeScopeResolver::class), - $context, - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $conditionalExpressions, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain, - $afterExtractCall, - $parentScope - ); - } - -} diff --git a/src/Analyser/LocalIgnoresProcessor.php b/src/Analyser/LocalIgnoresProcessor.php new file mode 100644 index 0000000000..192b909251 --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessor.php @@ -0,0 +1,103 @@ + $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 e031880a6c..c40ad28c13 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,23 +22,59 @@ 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\NativeTypeExpr; +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\PhpVersionFactory; +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; @@ -46,228 +85,168 @@ 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\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\HasOffsetValueType; +use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; -use PHPStan\Type\ClassStringType; use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; +use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\StaticType; -use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use stdClass; +use Throwable; +use ValueError; +use function abs; +use function array_filter; use function array_key_exists; - -class MutatingScope implements Scope +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_decrement; +use function str_increment; +use function str_starts_with; +use function strlen; +use function strtolower; +use function substr; +use function uksort; +use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; + +final class MutatingScope implements Scope { - public const CALCULATE_SCALARS_LIMIT = 128; - - private const OPERATOR_SIGIL_MAP = [ - Node\Expr\AssignOp\Plus::class => '+', - Node\Expr\AssignOp\Minus::class => '-', - 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 const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; + private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; - private Parser $parser; - - private NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Analyser\ScopeContext $context; - - /** @var \PHPStan\Type\Type[] */ + /** @var Type[] */ private array $resolvedTypes = []; - private bool $declareStrictTypes; - - /** @var array */ - private array $constantTypes; + /** @var array */ + private array $truthyScopes = []; - /** @var \PHPStan\Reflection\FunctionReflection|MethodReflection|null */ - private $function; + /** @var array */ + private array $falseyScopes = []; + /** @var non-empty-string|null */ private ?string $namespace; - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $variableTypes; - - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $moreSpecificTypes; - - /** @var array */ - private array $conditionalExpressions; - - private ?string $inClosureBindScopeClass; - - private ?ParametersAcceptor $anonymousFunctionReflection; + private ?self $scopeOutOfFirstLevelStatement = null; - private bool $inFirstLevelStatement; + private ?self $scopeWithPromotedNativeTypes = null; - /** @var array */ - private array $currentlyAssignedExpressions; - - /** @var array */ - private array $inFunctionCallsStack; - - /** @var array */ - private array $nativeExpressionTypes; - - /** @var string[] */ - private array $dynamicConstantNames; - - private bool $treatPhpDocTypesAsCertain; - - private bool $afterExtractCall; - - private ?Scope $parentScope; + 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 NodeScopeResolver $nodeScopeResolver - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes + * @param int|array{min: int, max: int}|null $configPhpVersion + * @param array $expressionTypes * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement + * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * @param string[] $dynamicConstantNames - * @param bool $treatPhpDocTypesAsCertain - * @param bool $afterExtractCall - * @param Scope|null $parentScope + * @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, - NodeScopeResolver $nodeScopeResolver, - 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 = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - array $dynamicConstantNames = [], - bool $treatPhpDocTypesAsCertain = true, - bool $afterExtractCall = false, - ?Scope $parentScope = null + 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->nodeScopeResolver = $nodeScopeResolver; - $this->context = $context; - $this->declareStrictTypes = $declareStrictTypes; - $this->constantTypes = $constantTypes; - $this->function = $function; $this->namespace = $namespace; - $this->variableTypes = $variablesTypes; - $this->moreSpecificTypes = $moreSpecificTypes; - $this->conditionalExpressions = $conditionalExpressions; - $this->inClosureBindScopeClass = $inClosureBindScopeClass; - $this->anonymousFunctionReflection = $anonymousFunctionReflection; - $this->inFirstLevelStatement = $inFirstLevelStatement; - $this->currentlyAssignedExpressions = $currentlyAssignedExpressions; - $this->nativeExpressionTypes = $nativeExpressionTypes; - $this->inFunctionCallsStack = $inFunctionCallsStack; - $this->dynamicConstantNames = $dynamicConstantNames; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->afterExtractCall = $afterExtractCall; - $this->parentScope = $parentScope; } /** @api */ @@ -286,20 +265,20 @@ public function getFileDescription(): string /** @var ClassReflection $classReflection */ $classReflection = $this->context->getClassReflection(); - $className = sprintf('class %s', $classReflection->getDisplayName()); - if ($classReflection->isAnonymous()) { - $className = 'anonymous class'; + $className = $classReflection->getDisplayName(); + if (!$classReflection->isAnonymous()) { + $className = sprintf('class %s', $className); } $traitReflection = $this->context->getTraitReflection(); if ($traitReflection->getFileName() === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return sprintf( '%s (in context of %s)', $traitReflection->getFileName(), - $className + $className, ); } @@ -314,13 +293,107 @@ public function enterDeclareStrictTypes(): self return $this->scopeFactory->create( $this->context, true, - [], null, null, - $this->variableTypes + $this->expressionTypes, + $this->nativeExpressionTypes, + ); + } + + /** + * @param array $currentExpressionTypes + * @return array + */ + private function rememberConstructorExpressions(array $currentExpressionTypes): array + { + $expressionTypes = []; + foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + if ($expr instanceof FuncCall) { + if ( + !$expr->name instanceof Name + || !in_array($expr->name->name, ['class_exists', 'function_exists'], true) + ) { + continue; + } + } elseif ($expr instanceof PropertyFetch) { + if (!$this->isReadonlyPropertyFetch($expr, true)) { + 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, ); } + private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool + { + if (!$this->phpVersion->supportsReadOnlyProperties()) { + return false; + } + + while ($expr instanceof PropertyFetch) { + if ($expr->var instanceof Variable) { + if ( + $allowOnlyOnThis + && ( + ! $expr->name instanceof Node\Identifier + || !is_string($expr->var->name) + || $expr->var->name !== 'this' + ) + ) { + return false; + } + } elseif (!$expr->var instanceof PropertyFetch) { + return false; + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection === null) { + return false; + } + + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + return false; + } + + $expr = $expr->var; + } + + return true; + } + /** @api */ public function isInClass(): bool { @@ -347,9 +420,8 @@ public function getTraitReflection(): ?ClassReflection /** * @api - * @return \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null */ - public function getFunction() + public function getFunction(): ?PhpFunctionFromParserNodeReflection { return $this->function; } @@ -372,14 +444,6 @@ public function getParentScope(): ?Scope return $this->parentScope; } - /** - * @return array - */ - private function getVariableTypes(): array - { - return $this->variableTypes; - } - /** @api */ public function canAnyVariableExist(): bool { @@ -391,27 +455,27 @@ public function afterExtractCall(): self return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, true, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } public function afterClearstatcacheCall(): self { - $moreSpecificTypes = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypes) as $exprString) { + $expressionTypes = $this->expressionTypes; + foreach (array_keys($expressionTypes) as $exprString) { // list from https://www.php.net/manual/en/function.clearstatcache.php // stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime(), fileinode(), filegroup(), fileowner(), filesize(), filetype(), and fileperms(). @@ -420,6 +484,7 @@ public function afterClearstatcacheCall(): self 'lstat', 'file_exists', 'is_writable', + 'is_writeable', 'is_readable', 'is_executable', 'is_file', @@ -435,31 +500,113 @@ public function afterClearstatcacheCall(): self 'filetype', 'fileperms', ] as $functionName) { - if (!Strings::startsWith((string) $exprString, $functionName . '(') && !Strings::startsWith((string) $exprString, '\\' . $functionName . '(')) { + if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) { continue; } - unset($moreSpecificTypes[$exprString]); + unset($expressionTypes[$exprString]); continue 2; } } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypes, + $expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + public function afterOpenSslCall(string $openSslFunctionName): self + { + $expressionTypes = $this->expressionTypes; + + if (in_array($openSslFunctionName, [ + 'openssl_cipher_iv_length', + 'openssl_cms_decrypt', + 'openssl_cms_encrypt', + 'openssl_cms_read', + 'openssl_cms_sign', + 'openssl_cms_verify', + 'openssl_csr_export_to_file', + 'openssl_csr_export', + 'openssl_csr_get_public_key', + 'openssl_csr_get_subject', + 'openssl_csr_new', + 'openssl_csr_sign', + 'openssl_decrypt', + 'openssl_dh_compute_key', + 'openssl_digest', + 'openssl_encrypt', + 'openssl_get_curve_names', + 'openssl_get_privatekey', + 'openssl_get_publickey', + 'openssl_open', + 'openssl_pbkdf2', + 'openssl_pkcs12_export_to_file', + 'openssl_pkcs12_export', + 'openssl_pkcs12_read', + 'openssl_pkcs7_decrypt', + 'openssl_pkcs7_encrypt', + 'openssl_pkcs7_read', + 'openssl_pkcs7_sign', + 'openssl_pkcs7_verify', + 'openssl_pkey_derive', + 'openssl_pkey_export_to_file', + 'openssl_pkey_export', + 'openssl_pkey_get_private', + 'openssl_pkey_get_public', + 'openssl_pkey_new', + 'openssl_private_decrypt', + 'openssl_private_encrypt', + 'openssl_public_decrypt', + 'openssl_public_encrypt', + 'openssl_random_pseudo_bytes', + 'openssl_seal', + 'openssl_sign', + 'openssl_spki_export_challenge', + 'openssl_spki_export', + 'openssl_spki_new', + 'openssl_spki_verify', + 'openssl_verify', + 'openssl_x509_checkpurpose', + 'openssl_x509_export_to_file', + 'openssl_x509_export', + 'openssl_x509_fingerprint', + 'openssl_x509_read', + 'openssl_x509_verify', + ], true)) { + unset($expressionTypes['\openssl_error_string()']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -470,7 +617,8 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createYes(); } - if (!isset($this->variableTypes[$variableName])) { + $varExprString = '$' . $variableName; + if (!isset($this->expressionTypes[$varExprString])) { if ($this->canAnyVariableExist()) { return TrinaryLogic::createMaybe(); } @@ -478,40 +626,80 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createNo(); } - return $this->variableTypes[$variableName]->getCertainty(); + return $this->expressionTypes[$varExprString]->getCertainty(); } /** @api */ public function getVariableType(string $variableName): Type { - if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new StringType(), new MixedType()); + if ($this->hasVariableType($variableName)->maybe()) { + if ($variableName === 'argc') { + return IntegerRangeType::fromInterval(1, null); + } + if ($variableName === 'argv') { + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), new StringType()), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ); + } + if ($this->canAnyVariableExist()) { + return new MixedType(); + } } if ($this->hasVariableType($variableName)->no()) { - throw new \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 array + * @return list */ public function getDefinedVariables(): array { $variables = []; - foreach ($this->variableTypes as $variableName => $holder) { + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } if (!$holder->getCertainty()->yes()) { continue; } - $variables[] = $variableName; + $variables[] = substr($exprString, 1); + } + + return $variables; + } + + /** + * @api + * @return list + */ + public function getMaybeDefinedVariables(): array + { + $variables = []; + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } + if (!$holder->getCertainty()->maybe()) { + continue; + } + + $variables[] = substr($exprString, 1); } return $variables; @@ -519,17 +707,7 @@ public function getDefinedVariables(): array private function isGlobalVariable(string $variableName): bool { - return in_array($variableName, [ - 'GLOBALS', - '_SERVER', - '_GET', - '_POST', - '_FILES', - '_COOKIE', - '_SESSION', - '_REQUEST', - '_ENV', - ], true); + return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } /** @api */ @@ -539,38 +717,19 @@ public function hasConstant(Name $name): bool if ($isCompilerHaltOffset) { return $this->fileHasCompilerHaltStatementCalls(); } - if ($name->isFullyQualified()) { - if (array_key_exists($name->toCodeString(), $this->constantTypes)) { - return true; - } - } - - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $name->toString()]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return true; - } - } - $constantName = new FullyQualified($name->toString()); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + if ($this->getGlobalConstantType($name) !== null) { return true; } - if (!$this->reflectionProvider->hasConstant($name, $this)) { - return false; - } - - $constantReflection = $this->reflectionProvider->getConstant($name, $this); - - return $constantReflection->getFileName() !== $this->getFile(); + return $this->reflectionProvider->hasConstant($name, $this); } private function fileHasCompilerHaltStatementCalls(): bool { $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; } } @@ -591,7 +750,7 @@ public function getAnonymousFunctionReflection(): ?ParametersAcceptor } /** @api */ - public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type + public function getAnonymousFunctionReturnType(): ?Type { if ($this->anonymousFunctionReflection === null) { return null; @@ -603,104 +762,188 @@ public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type /** @api */ public function getType(Expr $node): Type { + if ($node instanceof GetIterableKeyTypeExpr) { + return $this->getIterableKeyType($this->getType($node->getExpr())); + } + if ($node instanceof GetIterableValueTypeExpr) { + return $this->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 NativeTypeExpr) { + if ($this->nativeTypesPromoted) { + return $node->getNativeType(); + } + return $node->getPhpDocType(); + } + + 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 + { + $parts = []; + foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + $parts[] = '---'; + foreach ($this->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + + $parts[] = sprintf(':%d', count($this->inFunctionCallsStack)); + foreach ($this->inFunctionCallsStack as [$method, $parameter]) { + if ($parameter === null) { + $parts[] = ',null'; + continue; + } + + $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache())); + } + + return md5(implode("\n", $parts)); + } + + private function resolveType(string $exprString, Expr $node): Type { + foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) { + $type = $extension->getType($node, $this); + if ($type !== null) { + return $type; + } + } + if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) { - return new NeverType(true); + return new NonAcceptingNeverType(); + } + + if (!$node instanceof Variable && $this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } + + if ($node instanceof AlwaysRememberedExpr) { + return $this->nativeTypesPromoted ? $node->getNativeExprType() : $node->getExprType(); } if ($node instanceof Expr\BinaryOp\Smaller) { - return $this->getType($node->left)->isSmallerThan($this->getType($node->right))->toBooleanType(); + return $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\SmallerOrEqual) { - return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right))->toBooleanType(); + return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\Greater) { - return $this->getType($node->right)->isSmallerThan($this->getType($node->left))->toBooleanType(); + return $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\GreaterOrEqual) { - return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left))->toBooleanType(); + return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType(); } - if ( - $node instanceof Expr\BinaryOp\Equal - || $node instanceof Expr\BinaryOp\NotEqual - || $node instanceof Expr\Empty_ - ) { - return new BooleanType(); - } + if ($node instanceof Expr\BinaryOp\Equal) { + if ( + $node->left instanceof Variable + && is_string($node->left->name) + && $node->right instanceof Variable + && is_string($node->right->name) + && $node->left->name === $node->right->name + ) { + return new ConstantBooleanType(true); + } - if ($node instanceof Expr\Isset_) { - $result = new ConstantBooleanType(true); - foreach ($node->vars as $var) { - if ($var instanceof Expr\ArrayDimFetch && $var->dim !== null) { - $variableType = $this->getType($var->var); - $dimType = $this->getType($var->dim); - $hasOffset = $variableType->hasOffsetValueType($dimType); - $offsetValueType = $variableType->getOffsetValueType($dimType); - $offsetValueIsNotNull = (new NullType())->isSuperTypeOf($offsetValueType)->negate(); - $isset = $hasOffset->and($offsetValueIsNotNull)->toBooleanType(); - if ($isset instanceof ConstantBooleanType) { - if (!$isset->getValue()) { - return $isset; - } + $leftType = $this->getType($node->left); + $rightType = $this->getType($node->right); - continue; - } + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + } - $result = $isset; - continue; - } + if ($node instanceof Expr\BinaryOp\NotEqual) { + return $this->getType(new Expr\BooleanNot(new BinaryOp\Equal($node->left, $node->right))); + } - if ($var instanceof Expr\Variable && is_string($var->name)) { - $variableType = $this->getType($var); - $isNullSuperType = (new NullType())->isSuperTypeOf($variableType); - $has = $this->hasVariableType($var->name); - if ($has->no() || $isNullSuperType->yes()) { - return new ConstantBooleanType(false); - } + 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 ($has->maybe() || !$isNullSuperType->no()) { - $result = new BooleanType(); - } - continue; + 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()); } @@ -708,60 +951,35 @@ private function resolveType(Expr $node): Type return new BooleanType(); } - if ($node instanceof \PhpParser\Node\Expr\BitwiseNot) { - $exprType = $this->getType($node->expr); - return TypeTraverser::map($exprType, static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof ConstantStringType) { - return new ConstantStringType(~$type->getValue()); - } - if ($type instanceof StringType) { - return new StringType(); - } - if ($type instanceof IntegerType || $type instanceof FloatType) { - return new IntegerType(); //no const types here, result depends on PHP_INT_SIZE - } - return new ErrorType(); - }); + 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); } @@ -770,39 +988,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); } @@ -810,21 +1019,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(), ); } @@ -832,106 +1036,18 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Identical) { - if ($this->treatPhpDocTypesAsCertain) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - } else { - $leftType = $this->getNativeType($node->left); - $rightType = $this->getNativeType($node->right); - } + return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type; + } + + if ($node instanceof Expr\BinaryOp\NotIdentical) { + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type; + } + if ($node instanceof Expr\Instanceof_) { + $expressionType = $this->getType($node->expr); if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType instanceof NullType - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - $isSuperset = $leftType->isSuperTypeOf($rightType); - if ($isSuperset->no()) { - return new ConstantBooleanType(false); - } elseif ( - $isSuperset->yes() - && $leftType instanceof ConstantScalarType - && $rightType instanceof ConstantScalarType - && $leftType->getValue() === $rightType->getValue() - ) { - return new ConstantBooleanType(true); - } - - return new BooleanType(); - } - - if ($node instanceof Expr\BinaryOp\NotIdentical) { - if ($this->treatPhpDocTypesAsCertain) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - } else { - $leftType = $this->getNativeType($node->left); - $rightType = $this->getNativeType($node->right); - } - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType instanceof NullType - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - $isSuperset = $leftType->isSuperTypeOf($rightType); - if ($isSuperset->no()) { - return new ConstantBooleanType(true); - } elseif ( - $isSuperset->yes() - && $leftType instanceof ConstantScalarType - && $rightType instanceof ConstantScalarType - && $leftType->getValue() === $rightType->getValue() - ) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); - } - - if ($node instanceof Expr\Instanceof_) { - if ($this->treatPhpDocTypesAsCertain) { - $expressionType = $this->getType($node->expr); - } else { - $expressionType = $this->getNativeType($node->expr); - } - if ( - $this->isInTrait() - && TypeUtils::findThisType($expressionType) !== null + $this->isInTrait() + && TypeUtils::findThisType($expressionType) !== null ) { return new BooleanType(); } @@ -958,7 +1074,7 @@ private function resolveType(Expr $node): Type if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { $uncertainty = true; return $type; } @@ -999,467 +1115,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); - } - - if ($type instanceof IntegerRangeType) { - return $this->resolveType(new Node\Expr\BinaryOp\Mul($node->expr, new LNumber(-1))); - } - - return $type; + return $this->initializerExprTypeResolver->getUnaryMinusType($node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\BinaryOp\Concat || $node instanceof Expr\AssignOp\Concat) { - if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; - } else { - $left = $node->left; - $right = $node->right; - } - - $leftStringType = $this->getType($left)->toString(); - $rightStringType = $this->getType($right)->toString(); - if (TypeCombinator::union( - $leftStringType, - $rightStringType - ) instanceof ErrorType) { - return new ErrorType(); - } + if ($node instanceof Expr\BinaryOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') { - return $rightStringType; - } + if ($node instanceof Expr\AssignOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') { - return $leftStringType; - } + if ($node instanceof BinaryOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) { - return $leftStringType->append($rightStringType); - } + if ($node instanceof Expr\AssignOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - $accessoryTypes = []; - if ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) { - $accessoryTypes[] = new AccessoryNonEmptyStringType(); - } + if ($node instanceof BinaryOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } + if ($node instanceof Expr\AssignOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if (count($accessoryTypes) > 0) { - $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); - } + if ($node instanceof BinaryOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new StringType(); + if ($node instanceof Expr\AssignOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($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 Expr\BinaryOp\Spaceship) { + return $this->initializerExprTypeResolver->getSpaceshipType($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 BinaryOp\Div) { + return $this->initializerExprTypeResolver->getDivType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - ( - $node instanceof Node\Expr\BinaryOp - || $node instanceof Node\Expr\AssignOp - ) && !$node instanceof Expr\BinaryOp\Coalesce && !$node instanceof Expr\AssignOp\Coalesce - ) { - if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; - } else { - $left = $node->left; - $right = $node->right; - } - - $leftTypes = TypeUtils::getConstantScalars($this->getType($left)); - $rightTypes = TypeUtils::getConstantScalars($this->getType($right)); - - $leftTypesCount = count($leftTypes); - $rightTypesCount = count($rightTypes); - if ($leftTypesCount > 0 && $rightTypesCount > 0) { - $resultTypes = []; - $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftType) { - foreach ($rightTypes as $rightType) { - $resultType = $this->calculateFromScalars($node, $leftType, $rightType); - if ($generalize) { - $resultType = TypeUtils::generalizeType($resultType, GeneralizePrecision::lessSpecific()); - } - $resultTypes[] = $resultType; - } - } - return TypeCombinator::union(...$resultTypes); - } + if ($node instanceof Expr\AssignOp\Div) { + return $this->initializerExprTypeResolver->getDivType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Expr\AssignOp\Mod) { - if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; - } else { - $left = $node->left; - $right = $node->right; - } - - $leftType = $this->getType($left); - $rightType = $this->getType($right); - - $integer = new IntegerType(); - $positiveInt = IntegerRangeType::fromInterval(0, null); - if ($integer->isSuperTypeOf($rightType)->yes()) { - $rangeMin = null; - $rangeMax = null; - - if ($rightType instanceof IntegerRangeType) { - $rangeMax = $rightType->getMax() !== null ? $rightType->getMax() - 1 : null; - } elseif ($rightType instanceof ConstantIntegerType) { - $rangeMax = $rightType->getValue() - 1; - } elseif ($rightType instanceof UnionType) { - foreach ($rightType->getTypes() as $type) { - if ($type instanceof IntegerRangeType) { - if ($type->getMax() === null) { - $rangeMax = null; - } else { - $rangeMax = max($rangeMax, $type->getMax()); - } - } elseif ($type instanceof ConstantIntegerType) { - $rangeMax = max($rangeMax, $type->getValue() - 1); - } - } - } + if ($node instanceof BinaryOp\Mod) { + return $this->initializerExprTypeResolver->getModType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($positiveInt->isSuperTypeOf($leftType)->yes()) { - $rangeMin = 0; - } elseif ($rangeMax !== null) { - $rangeMin = $rangeMax * -1; - } + if ($node instanceof Expr\AssignOp\Mod) { + return $this->initializerExprTypeResolver->getModType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return IntegerRangeType::fromInterval($rangeMin, $rangeMax); - } elseif ($positiveInt->isSuperTypeOf($leftType)->yes()) { - return IntegerRangeType::fromInterval(0, null); - } + if ($node instanceof BinaryOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof Expr\AssignOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\BinaryOp\Spaceship) { - return IntegerRangeType::fromInterval(-1, 1); + if ($node instanceof BinaryOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\Clone_) { - return $this->getType($node->expr); + if ($node instanceof Expr\AssignOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->var, $node->expr, 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 ($node instanceof BinaryOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if (TypeCombinator::union( - $this->getType($left)->toNumber(), - $this->getType($right)->toNumber() - ) instanceof ErrorType) { - return new ErrorType(); - } + if ($node instanceof Expr\AssignOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof BinaryOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($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\Pow) { + return $this->initializerExprTypeResolver->getPowType($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\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($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\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($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\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof Expr\AssignOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($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; - } - - $leftType = $this->getType($left); - $rightType = $this->getType($right); - - if ($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus) { - $leftConstantArrays = TypeUtils::getConstantArrays($leftType); - $rightConstantArrays = TypeUtils::getConstantArrays($rightType); - - if (count($leftConstantArrays) > 0 && count($rightConstantArrays) > 0) { - $resultTypes = []; - foreach ($rightConstantArrays as $rightConstantArray) { - foreach ($leftConstantArrays as $leftConstantArray) { - $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($rightConstantArray); - foreach ($leftConstantArray->getKeyTypes() as $leftKeyType) { - $newArrayBuilder->setOffsetValueType( - $leftKeyType, - $leftConstantArray->getOffsetValueType($leftKeyType) - ); - } - $resultTypes[] = $newArrayBuilder->getArray(); - } - } + if ($node instanceof Expr\Clone_) { + return $this->getType($node->expr); + } - return TypeCombinator::union(...$resultTypes); + 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(); } - $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()) - ); + if ($resultType === null) { + $resultType = $partType; + continue; } - if ($leftType instanceof MixedType && $rightType instanceof MixedType) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - new ArrayType(new MixedType(), new MixedType()), - ]); - } + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); } - if (($leftType instanceof IntegerRangeType || $leftType instanceof ConstantIntegerType || $leftType instanceof UnionType) && - ($rightType instanceof IntegerRangeType || $rightType instanceof ConstantIntegerType || $rightType instanceof UnionType) && - !($node instanceof Node\Expr\BinaryOp\Pow || $node instanceof Node\Expr\AssignOp\Pow)) { - - if ($leftType instanceof ConstantIntegerType) { - return $this->integerRangeMath( - $leftType, - $node, - $rightType - ); - } elseif ($leftType instanceof UnionType) { - - $unionParts = []; - - foreach ($leftType->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($type, $node, $rightType); - } else { - $unionParts[] = $type; - } - } - - $union = TypeCombinator::union(...$unionParts); - if ($leftType instanceof BenevolentUnionType) { - return TypeUtils::toBenevolentUnion($union)->toNumber(); + return $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 $union->toNumber(); + return new ObjectType(Closure::class); } - return $this->integerRangeMath($leftType, $node, $rightType); - } - - $operatorSigil = null; - - if ($node instanceof BinaryOp) { - $operatorSigil = $node->getOperatorSigil(); - } + $callableType = $this->getType($node->name); + if (!$callableType->isCallable()->yes()) { + return new ObjectType(Closure::class); + } - if ($operatorSigil === null) { - $operatorSigil = self::OPERATOR_SIGIL_MAP[get_class($node)] ?? null; + return $this->createFirstClassCallable( + null, + $callableType->getCallableParametersAcceptors($this), + ); } - if ($operatorSigil !== null) { - $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistry->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); - - /** @var Type[] $extensionTypes */ - $extensionTypes = []; - - foreach ($operatorTypeSpecifyingExtensions as $extension) { - $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); + if ($node instanceof MethodCall) { + if (!$node->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); } - if (count($extensionTypes) > 0) { - return TypeCombinator::union(...$extensionTypes); + $varType = $this->getType($node->var); + $method = $this->getMethodReflection($varType, $node->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); } - } - - $types = TypeCombinator::union($leftType, $rightType); - if ( - $leftType instanceof ArrayType - || $rightType instanceof ArrayType - || $types instanceof ArrayType - ) { - return new ErrorType(); - } - - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); - - if ( - (new FloatType())->isSuperTypeOf($leftNumberType)->yes() - || (new FloatType())->isSuperTypeOf($rightNumberType)->yes() - ) { - return new FloatType(); - } - if ($node instanceof Expr\AssignOp\Pow || $node instanceof Expr\BinaryOp\Pow) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } - $resultType = TypeCombinator::union($leftNumberType, $rightNumberType); - if ($node instanceof Expr\AssignOp\Div || $node instanceof Expr\BinaryOp\Div) { - if ($types instanceof MixedType || $resultType instanceof IntegerType) { - return new BenevolentUnionType([new IntegerType(), new FloatType()]); + if ($node instanceof Expr\StaticCall) { + if (!$node->class instanceof Name) { + return new ObjectType(Closure::class); } - return new UnionType([new IntegerType(), new FloatType()]); - } - - if ($types instanceof MixedType - || $leftType instanceof BenevolentUnionType - || $rightType instanceof BenevolentUnionType - ) { - return TypeUtils::toBenevolentUnion($resultType); - } - - 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) { - $parts = []; - foreach ($node->parts as $part) { - if ($part instanceof EncapsedStringPart) { - $parts[] = new ConstantStringType($part->value); - continue; + if (!$node->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); } - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); + $classType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + $methodName = $node->name->toString(); + if (!$classType->hasMethod($methodName)->yes()) { + return new ObjectType(Closure::class); } - $parts[] = $partStringType; + $method = $classType->getMethod($methodName, $this); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } - $constantString = new ConstantStringType(''); - foreach ($parts as $part) { - if ($part instanceof ConstantStringType) { - $constantString = $constantString->append($part); - continue; - } - - foreach ($parts as $partType) { - if ($partType->isNonEmptyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); - } - } - - return new StringType(); + 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; @@ -1481,115 +1335,242 @@ 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, ); } $callableParameters = null; - $arg = $node->getAttribute('parent'); - if ($arg instanceof Arg) { - $funcCall = $arg->getAttribute('parent'); - $argOrder = $arg->getAttribute('expressionOrder'); - if ($funcCall instanceof FuncCall && $funcCall->name instanceof Name) { - $functionName = $this->reflectionProvider->resolveFunctionName($funcCall->name, $this); - if ( - $functionName === 'array_map' - && $argOrder === 0 - && isset($funcCall->getArgs()[1]) - ) { - if (!isset($funcCall->getArgs()[2])) { - $callableParameters = [ - new DummyParameter('item', $this->getType($funcCall->getArgs()[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ]; - } else { - $callableParameters = []; - foreach ($funcCall->getArgs() as $i => $funcCallArg) { - if ($i === 0) { - continue; - } - - $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); - } - } + $arrayMapArgs = $node->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { + $callableParameters = []; + foreach ($arrayMapArgs as $funcCallArg) { + $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } + } else { + $inFunctionCallsStackCount = count($this->inFunctionCallsStack); + if ($inFunctionCallsStackCount > 0) { + [, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; + if ($inParameter !== null) { + $callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType()); } } } if ($node instanceof Expr\ArrowFunction) { - $returnType = $this->enterArrowFunctionWithoutReflection($node, $callableParameters)->getType($node->expr); - if ($node->returnType !== null) { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); - } - } else { - $closureScope = $this->enterAnonymousFunctionWithoutReflection($node, $callableParameters); - $closureReturnStatements = []; - $closureYieldStatements = []; - $closureExecutionEnds = []; - $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } - - if ($node instanceof ExecutionEndNode) { - if ($node->getStatementResult()->isAlwaysTerminating()) { - foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { - if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { - continue; - } + $arrowScope = $this->enterArrowFunctionWithoutReflection($node, $callableParameters); - $closureExecutionEnds[] = $node; - break; - } + if ($node->expr instanceof Expr\Yield_ || $node->expr instanceof Expr\YieldFrom) { + $yieldNode = $node->expr; - if (count($node->getStatementResult()->getExitPoints()) === 0) { - $closureExecutionEnds[] = $node; - } + if ($yieldNode instanceof Expr\Yield_) { + if ($yieldNode->key === null) { + $keyType = new IntegerType(); + } else { + $keyType = $arrowScope->getType($yieldNode->key); } - return; + if ($yieldNode->value === null) { + $valueType = new NullType(); + } else { + $valueType = $arrowScope->getType($yieldNode->value); + } + } else { + $yieldFromType = $arrowScope->getType($yieldNode->expr); + $keyType = $arrowScope->getIterableKeyType($yieldFromType); + $valueType = $arrowScope->getIterableValueType($yieldFromType); } - if ($node instanceof Node\Stmt\Return_) { - $closureReturnStatements[] = [$node, $scope]; + $returnType = new GenericObjectType(Generator::class, [ + $keyType, + $valueType, + new MixedType(), + new VoidType(), + ]); + } else { + $returnType = $arrowScope->getKeepVoidType($node->expr); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); } + } - if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { - return; - } + $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; + } - $closureYieldStatements[] = [$node, $scope]; - }); + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } - $returnTypes = []; - $hasNull = false; - foreach ($closureReturnStatements as [$returnNode, $returnScope]) { - if ($returnNode->expr === null) { - $hasNull = true; - continue; + 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]; + + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'nodiscard') { + $mustUseReturnValue = TrinaryLogic::createYes(); + break; + } + } } - $returnTypes[] = $returnScope->getType($returnNode->expr); + return new ClosureType( + $parameters, + $cachedClosureData['returnType'], + $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + throwPoints: $cachedClosureData['throwPoints'], + impurePoints: $cachedClosureData['impurePoints'], + invalidateExpressions: $cachedClosureData['invalidateExpressions'], + usedVariables: $cachedClosureData['usedVariables'], + acceptsNamedArguments: TrinaryLogic::createYes(), + mustUseReturnValue: $mustUseReturnValue, + ); } - + 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 (count($closureExecutionEnds) > 0 && !$hasNull) { - $returnType = new NeverType(true); + if ($onlyNeverExecutionEnds === true && !$hasNull) { + $returnType = new NonAcceptingNeverType(); } else { $returnType = new VoidType(); } } else { - if (count($closureExecutionEnds) > 0) { - $returnTypes[] = new NeverType(true); + if ($onlyNeverExecutionEnds === true) { + $returnTypes[] = new NonAcceptingNeverType(); } if ($hasNull) { $returnTypes[] = new NullType(); @@ -1618,25 +1599,98 @@ private function resolveType(Expr $node): Type } $yieldFromType = $yieldScope->getType($yieldNode->expr); - $keyTypes[] = $yieldFromType->getIterableKeyType(); - $valueTypes[] = $yieldFromType->getIterableValueType(); + $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType); } - $returnType = new GenericObjectType(\Generator::class, [ + $returnType = new GenericObjectType(Generator::class, [ TypeCombinator::union(...$keyTypes), TypeCombinator::union(...$valueTypes), new MixedType(), $returnType, ]); } else { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } + } + + $usedVariables = []; + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $usedVariables[] = $use->var->name; + } + + foreach ($node->uses as $use) { + if (!$use->byRef) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref use', + true, + ); + break; + } + } + + foreach ($parameters as $parameter) { + if ($parameter->passedByReference()->no()) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref parameter', + true, + ); + } + + $throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints); + $impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints); + + $cachedTypes = $node->getAttribute('phpstanCachedTypes', []); + $cachedTypes[$this->getClosureScopeCacheKey()] = [ + 'returnType' => $returnType, + 'throwPoints' => $throwPointsForClosureType, + 'impurePoints' => $impurePointsForClosureType, + 'invalidateExpressions' => $invalidateExpressions, + 'usedVariables' => $usedVariables, + ]; + $node->setAttribute('phpstanCachedTypes', $cachedTypes); + + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'nodiscard') { + $mustUseReturnValue = TrinaryLogic::createYes(); + break; + } } } return new ClosureType( $parameters, $returnType, - $isVariadic + $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + throwPoints: $throwPointsForClosureType, + impurePoints: $impurePointsForClosureType, + invalidateExpressions: $invalidateExpressions, + usedVariables: $usedVariables, + acceptsNamedArguments: TrinaryLogic::createYes(), + mustUseReturnValue: $mustUseReturnValue, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1666,95 +1720,45 @@ private function resolveType(Expr $node): Type } $exprType = $this->getType($node->class); - return $this->getTypeToInstantiateForNew($exprType); + return $exprType->getObjectTypeOrClassStringObjectType(); } elseif ($node instanceof Array_) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - if (count($node->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); - } - foreach ($node->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - - $valueType = $this->getType($arrayItem->value); - if ($arrayItem->unpack) { - if ($valueType instanceof ConstantArrayType) { - foreach ($valueType->getValueTypes() as $innerValueType) { - $arrayBuilder->setOffsetValueType(null, $innerValueType); - } - } else { - $arrayBuilder->degradeToGeneralArray(); - $arrayBuilder->setOffsetValueType(new IntegerType(), $valueType->getIterableValueType()); - } - } else { - $arrayBuilder->setOffsetValueType( - $arrayItem->key !== null ? $this->getType($arrayItem->key) : null, - $valueType - ); - } - } - return $arrayBuilder->getArray(); - + return $this->initializerExprTypeResolver->getArrayType($node, fn (Expr $expr): Type => $this->getType($expr)); } elseif ($node instanceof Int_) { return $this->getType($node->expr)->toInteger(); } elseif ($node instanceof Bool_) { return $this->getType($node->expr)->toBoolean(); } elseif ($node instanceof Double) { return $this->getType($node->expr)->toFloat(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) { + } elseif ($node instanceof Node\Expr\Cast\String_) { return $this->getType($node->expr)->toString(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Array_) { + } elseif ($node instanceof Node\Expr\Cast\Array_) { return $this->getType($node->expr)->toArray(); - } elseif ($node instanceof Node\Scalar\MagicConst\Line) { - return new ConstantIntegerType($node->getLine()); - } elseif ($node instanceof Node\Scalar\MagicConst\Class_) { - if (!$this->isInClass()) { - return new ConstantStringType(''); - } - - return new ConstantStringType($this->getClassReflection()->getName(), true); - } elseif ($node instanceof Node\Scalar\MagicConst\Dir) { - return new ConstantStringType(dirname($this->getFile())); - } elseif ($node instanceof Node\Scalar\MagicConst\File) { - return new ConstantStringType($this->getFile()); - } elseif ($node instanceof Node\Scalar\MagicConst\Namespace_) { - return new ConstantStringType($this->namespace ?? ''); - } elseif ($node instanceof Node\Scalar\MagicConst\Method) { - if ($this->isInAnonymousFunction()) { - return new ConstantStringType('{closure}'); - } - - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); - } - if ($function instanceof MethodReflection) { - return new ConstantStringType( - sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()) - ); - } - - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Function_) { - if ($this->isInAnonymousFunction()) { - return new ConstantStringType('{closure}'); - } - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); - } - - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Trait_) { - if (!$this->isInTrait()) { - return new ConstantStringType(''); - } - return new ConstantStringType($this->getTraitReflection()->getName(), true); + } elseif ($node instanceof Node\Scalar\MagicConst) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Object_) { $castToObject = static function (Type $type): Type { - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $objects = []; + foreach ($constantArrays as $constantArray) { + $properties = []; + $optionalProperties = []; + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $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; } @@ -1773,92 +1777,217 @@ private function resolveType(Expr $node): Type return $this->getType($node->var); } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); - $varScalars = TypeUtils::getConstantScalars($varType); - $stringType = new StringType(); + $varScalars = $varType->getConstantScalarValues(); + if (count($varScalars) > 0) { $newTypes = []; - foreach ($varScalars as $scalar) { - $varValue = $scalar->getValue(); + foreach ($varScalars as $varValue) { + // until PHP 8.5 it was valid to increment/decrement an empty string. + // see https://github.com/php/php-src/issues/19597 if ($node instanceof Expr\PreInc) { - ++$varValue; + if ($varValue === '') { + $varValue = '1'; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_increment($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (!is_bool($varValue)) { + ++$varValue; + } } else { - --$varValue; + if ($varValue === '') { + $varValue = -1; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_decrement($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (is_numeric($varValue)) { + --$varValue; + } } $newTypes[] = $this->getTypeFromValue($varValue); } return TypeCombinator::union(...$newTypes); - } elseif ($stringType->isSuperTypeOf($varType)->yes()) { + } elseif ($varType->isString()->yes()) { if ($varType->isLiteralString()->yes()) { - return new IntersectionType([$stringType, new AccessoryLiteralStringType()]); + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); } - return $stringType; + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); } if ($node instanceof Expr\PreInc) { - return $this->getType(new BinaryOp\Plus($node->var, new LNumber(1))); + return $this->getType(new BinaryOp\Plus($node->var, new Node\Scalar\Int_(1))); } - return $this->getType(new BinaryOp\Minus($node->var, new LNumber(1))); + return $this->getType(new BinaryOp\Minus($node->var, new Node\Scalar\Int_(1))); } elseif ($node instanceof Expr\Yield_) { $functionReflection = $this->getFunction(); if ($functionReflection === null) { return new MixedType(); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!$returnType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorSendType = GenericTypeVariableResolver::getType($returnType, \Generator::class, 'TSend'); - if ($generatorSendType === null) { + $returnType = $functionReflection->getReturnType(); + $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); + if ($generatorSendType instanceof ErrorType) { return new MixedType(); } return $generatorSendType; } elseif ($node instanceof Expr\YieldFrom) { $yieldFromType = $this->getType($node->expr); - - if (!$yieldFromType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, \Generator::class, 'TReturn'); - if ($generatorReturnType === null) { + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { return new MixedType(); } return $generatorReturnType; } elseif ($node instanceof Expr\Match_) { $cond = $node->cond; + $condType = $this->getType($cond); $types = []; $matchScope = $this; - foreach ($node->arms as $arm) { + $arms = $node->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $conditionCases = []; + foreach ($arm->conds as $armCond) { + if (!$armCond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$armCond->class instanceof Name) { + continue 2; + } + if (!$armCond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $this->resolveName($armCond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + $caseName = $armCond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $conditionCases[] = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $types[] = $matchScope->addTypeToExpression( + $cond, + $conditionCaseType, + )->getType($arm->body); + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($cond, $remainingType); + } + } + + foreach ($arms as $arm) { if ($arm->conds === null) { + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } $types[] = $matchScope->getType($arm->body); continue; } if (count($arm->conds) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $filteringExpr = null; - foreach ($arm->conds as $armCond) { - $armCondExpr = new BinaryOp\Identical($cond, $armCond); - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; + if (count($arm->conds) === 1) { + $filteringExpr = new BinaryOp\Identical($cond, $arm->conds[0]); + } else { + $items = []; + foreach ($arm->conds as $filteringExpr) { + $items[] = new Node\ArrayItem($filteringExpr); } - - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + $filteringExpr = new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); } - $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); - $types[] = $truthyScope->getType($arm->body); + $filteringExprType = $matchScope->getType($filteringExpr); + + if (!$filteringExprType->isFalse()->yes()) { + $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } + $types[] = $truthyScope->getType($arm->body); + } $matchScope = $matchScope->filterByFalseyValue($filteringExpr); } @@ -1866,9 +1995,33 @@ private function resolveType(Expr $node): Type return TypeCombinator::union(...$types); } - $exprString = $this->getNodeKey($node); - if (isset($this->moreSpecificTypes[$exprString]) && $this->moreSpecificTypes[$exprString]->getCertainty()->yes()) { - return $this->moreSpecificTypes[$exprString]->getType(); + if ($node instanceof Expr\Isset_) { + $issetResult = true; + foreach ($node->vars as $var) { + $result = $this->issetCheck($var, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + if ($result !== null) { + if (!$result) { + return new ConstantBooleanType($result); + } + + continue; + } + + $issetResult = $result; + } + + if ($issetResult === null) { + return new BooleanType(); + } + + return new ConstantBooleanType($issetResult); } if ($node instanceof Expr\AssignOp\Coalesce) { @@ -1876,260 +2029,146 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Coalesce) { - if ($node->left instanceof Expr\ArrayDimFetch && $node->left->dim !== null) { - $dimType = $this->getType($node->left->dim); - $varType = $this->getType($node->left->var); - $hasOffset = $varType->hasOffsetValueType($dimType); - $leftType = $this->getType($node->left); - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))) - )->getType($node->right); - if ($hasOffset->no()) { - return $rightType; - } elseif ($hasOffset->yes()) { - $offsetValueType = $varType->getOffsetValueType($dimType); - if ($offsetValueType->isSuperTypeOf(new NullType())->no()) { - return TypeCombinator::removeNull($leftType); - } + $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 TypeCombinator::union( - TypeCombinator::removeNull($leftType), - $rightType - ); - } + return !$isNull->yes(); + }); - $leftType = $this->getType($node->left); - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))) - )->getType($node->right); - if ($leftType instanceof ErrorType || $leftType instanceof NullType) { - return $rightType; + if ($result !== null && $result !== false) { + return TypeCombinator::removeNull($leftType); } - if ( - TypeCombinator::containsNull($leftType) - || $node->left instanceof PropertyFetch - || ( - $node->left instanceof Variable - && is_string($node->left->name) - && !$this->hasVariableType($node->left->name)->yes() - ) - ) { + $rightType = $this->filterByFalseyValue($issetLeftExpr)->getType($node->right); + + if ($result === null) { return TypeCombinator::union( TypeCombinator::removeNull($leftType), - $rightType + $rightType, ); } - return TypeCombinator::removeNull($leftType); + return $rightType; } if ($node instanceof ConstFetch) { $constName = (string) $node->name; $loweredConstName = strtolower($constName); if ($loweredConstName === 'true') { - return new \PHPStan\Type\Constant\ConstantBooleanType(true); + return new ConstantBooleanType(true); } elseif ($loweredConstName === 'false') { - return new \PHPStan\Type\Constant\ConstantBooleanType(false); + return new ConstantBooleanType(false); } elseif ($loweredConstName === 'null') { return new NullType(); } - if ($node->name->isFullyQualified()) { - if (array_key_exists($node->name->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($node->name->toString(), $this->constantTypes[$node->name->toCodeString()]); - } - } - - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $constName]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); - } - } - - $constantName = new FullyQualified($constName); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + $namespacedName = null; + if (!$node->name->isFullyQualified() && $this->getNamespace() !== null) { + $namespacedName = new FullyQualified([$this->getNamespace(), $node->name->toString()]); } + $globalName = new FullyQualified($node->name->toString()); - if ($this->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 ($resolvedConstantName === 'PATH_SEPARATOR') { - return new UnionType([ - new ConstantStringType(':'), - new ConstantStringType(';'), - ]); - } - if ($resolvedConstantName === 'PHP_EOL') { - return new UnionType([ - new ConstantStringType("\n"), - new ConstantStringType("\r\n"), - ]); - } - if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { - return new IntegerType(); + foreach ([$namespacedName, $globalName] as $name) { + if ($name === null) { + continue; } - if ($resolvedConstantName === 'PHP_INT_MAX') { - return IntegerRangeType::fromInterval(1, null); + $constFetch = new ConstFetch($name); + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->constantResolver->resolveConstantType( + $name->toString(), + $this->expressionTypes[$this->getNodeKey($constFetch)]->getType(), + ); } + } - $constantType = $this->reflectionProvider->getConstant($node->name, $this)->getValueType(); - - return $this->resolveConstantType($resolvedConstantName, $constantType); + $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) { - $constantName = $node->name->name; - $isObject = false; - if ($node->class instanceof Name) { - $constantClass = (string) $node->class; - $constantClassType = new ObjectType($constantClass); - $namesToResolve = [ - 'self', - 'parent', - ]; - if ($this->isInClass()) { - if ($this->getClassReflection()->isFinal()) { - $namesToResolve[] = 'static'; - } elseif (strtolower($constantClass) === 'static') { - if (strtolower($constantName) === 'class') { - return new GenericClassStringType(new StaticType($this->getClassReflection())); - } + 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), + ); + } - $namesToResolve[] = 'static'; - $isObject = true; - } - } - if (in_array(strtolower($constantClass), $namesToResolve, true)) { - $resolvedName = $this->resolveName($node->class); - if ($resolvedName === 'parent' && strtolower($constantName) === 'class') { - return new ClassStringType(); - } - $constantClassType = $this->resolveTypeByName($node->class); + if ($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 (strtolower($constantName) === 'class') { - return new ConstantStringType($constantClassType->getClassName(), true); + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); } - } else { - $constantClassType = $this->getType($node->class); - $isObject = true; + + return TypeCombinator::union( + TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($node->cond)), + $condResult->getFalseyScope()->getType($node->else), + ); } - $referencedClasses = TypeUtils::getDirectClassNames($constantClassType); - if (strtolower($constantName) === 'class') { - if (count($referencedClasses) === 0) { - return new ErrorType(); - } - $classTypes = []; - foreach ($referencedClasses as $referencedClass) { - $classTypes[] = new GenericClassStringType(new ObjectType($referencedClass)); - } + $booleanConditionType = $this->getType($node->cond)->toBoolean(); + if ($booleanConditionType->isTrue()->yes()) { + return $condResult->getTruthyScope()->getType($node->if); + } - return TypeCombinator::union(...$classTypes); + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); } - $types = []; - foreach ($referencedClasses as $referencedClass) { - if (!$this->reflectionProvider->hasClass($referencedClass)) { - continue; - } - $constantClassReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$constantClassReflection->hasConstant($constantName)) { - continue; - } + return TypeCombinator::union( + $condResult->getTruthyScope()->getType($node->if), + $condResult->getFalseyScope()->getType($node->else), + ); + } - $constantReflection = $constantClassReflection->getConstant($constantName); - if ( - $constantReflection instanceof ClassConstantReflection - && $isObject - && !$constantClassReflection->isFinal() - && !$constantReflection->hasPhpDocType() - ) { - return new MixedType(); + if ($node instanceof Variable) { + if (is_string($node->name)) { + if ($this->hasVariableType($node->name)->no()) { + return new ErrorType(); } - if ( - $isObject - && ( - !$constantReflection instanceof ClassConstantReflection - || !$constantClassReflection->isFinal() - ) - ) { - $constantType = $constantReflection->getValueType(); - } else { - $constantType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); - } + return $this->getVariableType($node->name); + } - if ( - $constantType instanceof ConstantType - && in_array(sprintf('%s::%s', $constantClassReflection->getName(), $constantName), $this->dynamicConstantNames, true) - ) { - $constantType = $constantType->generalize(GeneralizePrecision::lessSpecific()); + $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()); } - $types[] = $constantType; - } - if (count($types) > 0) { return TypeCombinator::union(...$types); } - - if (!$constantClassType->hasConstant($constantName)->yes()) { - return new ErrorType(); - } - - return $constantClassType->getConstant($constantName)->getValueType(); - } - - if ($node instanceof Expr\Ternary) { - if ($node->if === null) { - $conditionType = $this->getType($node->cond); - $booleanConditionType = $conditionType->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->cond); - } - - return $this->filterByFalseyValue($node->cond)->getType($node->else); - } - return TypeCombinator::union( - TypeCombinator::remove($this->filterByTruthyValue($node->cond)->getType($node->cond), StaticTypeFactory::falsey()), - $this->filterByFalseyValue($node->cond)->getType($node->else) - ); - } - - $booleanConditionType = $this->getType($node->cond)->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->if); - } - - return $this->filterByFalseyValue($node->cond)->getType($node->else); - } - - return TypeCombinator::union( - $this->filterByTruthyValue($node->cond)->getType($node->if), - $this->filterByFalseyValue($node->cond)->getType($node->else) - ); - } - - if ($node instanceof Variable && is_string($node->name)) { - if ($this->hasVariableType($node->name)->no()) { - return new ErrorType(); - } - - return $this->getVariableType($node->name); - } + } if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { return $this->getNullsafeShortCircuitingType( @@ -2137,29 +2176,53 @@ private function resolveType(Expr $node): Type $this->getTypeFromArrayDimFetch( $node, $this->getType($node->dim), - $this->getType($node->var) - ) + $this->getType($node->var), + ), ); } - if ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { + if ($node instanceof MethodCall) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $methodReflection = $this->getMethodReflection( + $this->getNativeType($node->var), + $node->name->name, + ); + if ($methodReflection === null) { + $returnType = new ErrorType(); + } else { + $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } + + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } + $returnType = $this->methodCallReturnType( $this->getType($node->var), $node->name->name, - $node + $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new MethodCall($node->var, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafeMethodCall) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new MethodCall($node->var, $node->name, $node->args)); } @@ -2167,58 +2230,113 @@ private function resolveType(Expr $node): Type return TypeCombinator::union( $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) ->getType(new MethodCall($node->var, $node->name, $node->args)), - new NullType() + new NullType(), ); } - if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { + if ($node instanceof Expr\StaticCall) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + if ($node->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + } else { + $staticMethodCalledOnType = $this->getNativeType($node->class); + } + $methodReflection = $this->getMethodReflection( + $staticMethodCalledOnType, + $node->name->name, + ); + if ($methodReflection === null) { + $callType = new ErrorType(); + } else { + $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } + + return $callType; + } + if ($node->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByName($node->class); + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); } else { - $staticMethodCalledOnType = $this->getType($node->class); - if ($staticMethodCalledOnType instanceof GenericClassStringType) { - $staticMethodCalledOnType = $staticMethodCalledOnType->getGenericType(); - } + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->methodCallReturnType( + $callType = $this->methodCallReturnType( $staticMethodCalledOnType, $node->name->toString(), - $node + $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($callType === null) { + $callType = new ErrorType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); } - return $returnType; - }; - $callType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $callType); + return $callType; } - return $callType; + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticCall($node->class, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()), + ); + } } - if ($node instanceof PropertyFetch && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { + if ($node instanceof PropertyFetch) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + $nativeType = $propertyReflection->getNativeType(); + + return $this->getNullsafeShortCircuitingType($node->var, $nativeType); + } + $returnType = $this->propertyFetchType( $this->getType($node->var), $node->name->name, - $node + $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } + + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType( + new PropertyFetch($node->var, new Identifier($constantString->getValue())), + ), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafePropertyFetch) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new PropertyFetch($node->var, $node->name)); } @@ -2226,41 +2344,60 @@ private function resolveType(Expr $node): Type return TypeCombinator::union( $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) ->getType(new PropertyFetch($node->var, $node->name)), - new NullType() + new NullType(), ); } - if ( - $node instanceof Expr\StaticPropertyFetch - && $node->name instanceof Node\VarLikeIdentifier - ) { - $typeCallback = function () use ($node): Type { + if ($node instanceof Expr\StaticPropertyFetch) { + if ($node->name instanceof Node\VarLikeIdentifier) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + $nativeType = $propertyReflection->getNativeType(); + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $nativeType); + } + + return $nativeType; + } + if ($node->class instanceof Name) { $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); } else { - $staticPropertyFetchedOnType = $this->getType($node->class); - if ($staticPropertyFetchedOnType instanceof GenericClassStringType) { - $staticPropertyFetchedOnType = $staticPropertyFetchedOnType->getGenericType(); - } + $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->propertyFetchType( + $fetchType = $this->propertyFetchType( $staticPropertyFetchedOnType, $node->name->toString(), - $node + $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($fetchType === null) { + $fetchType = new ErrorType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $fetchType); } - return $returnType; - }; - $fetchType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + return $fetchType; } - return $fetchType; + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticPropertyFetch($node->class, new Node\VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), + ); + } } if ($node instanceof FuncCall) { @@ -2270,11 +2407,35 @@ private function resolveType(Expr $node): Type return new ErrorType(); } - return ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), - $calledOnType->getCallableParametersAcceptors($this) - )->getReturnType(); + $calledOnType->getCallableParametersAcceptors($this), + null, + ); + + $functionName = null; + if ($node->name instanceof String_) { + /** @var non-empty-string $name */ + $name = $node->name->value; + $functionName = new Name($name); + } elseif ( + $node->name instanceof FuncCall + && $node->name->name instanceof Name + && $node->name->isFirstClassCallable() + ) { + $functionName = $node->name->name; + } + + if ($functionName !== null && $this->reflectionProvider->hasFunction($functionName, $this)) { + $functionReflection = $this->reflectionProvider->getFunction($functionName, $this); + $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $node, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; + } + } + + return $parametersAcceptor->getReturnType(); } if (!$this->reflectionProvider->hasFunction($node->name, $this)) { @@ -2282,24 +2443,62 @@ private function resolveType(Expr $node): Type } $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; - } + if ($this->nativeTypesPromoted) { + return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); + } + + if ($functionReflection->getName() === 'call_user_func') { + $result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this); + if ($result !== null) { + [, $innerFuncCall] = $result; - return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this); + return $this->getType($innerFuncCall); + } } - return ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), - $functionReflection->getVariants() - )->getReturnType(); + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedNode !== null) { + $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $normalizedNode, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; + } + } + + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node); } return new MixedType(); } + private function getDynamicFunctionReturnType(ParametersAcceptor $parametersAcceptor, FuncCall $node, FunctionReflection $functionReflection): ?Type + { + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedNode !== null) { + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { + if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { + continue; + } + + $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( + $functionReflection, + $node, + $this, + ); + if ($resolvedType !== null) { + return $resolvedType; + } + } + } + + return null; + } + private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type { if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { @@ -2334,250 +2533,342 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type return $type; } - private function resolveConstantType(string $constantName, Type $constantType): Type + private function transformVoidToNull(Type $type, Node $node): Type { - if ($constantType instanceof ConstantType && in_array($constantName, $this->dynamicConstantNames, true)) { - return $constantType->generalize(GeneralizePrecision::lessSpecific()); + if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + return $type; } - return $constantType; + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type->isVoid()->yes()) { + return new NullType(); + } + + return $type; + }); } - /** @api */ - public function getNativeType(Expr $expr): Type + /** + * @param callable(Type): ?bool $typeCallback + */ + public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool { - $key = $this->getNodeKey($expr); + // 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 (array_key_exists($key, $this->nativeExpressionTypes)) { - return $this->nativeExpressionTypes[$key]; - } + if ($result === null) { + if ($hasVariable->yes()) { + if ($expr->name === '_SESSION') { + return null; + } - if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - return $this->getNullsafeShortCircuitingType( - $expr->var, - $this->getTypeFromArrayDimFetch( - $expr, - $this->getNativeType($expr->dim), - $this->getNativeType($expr->var) - ) - ); - } + return $typeCallback($this->getVariableType($expr->name)); + } - return $this->getType($expr); - } + return false; + } - /** @api */ - public function doNotTreatPhpDocTypesAsCertain(): Scope - { - if (!$this->treatPhpDocTypesAsCertain) { - return $this; - } + 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); + } - return new self( - $this->scopeFactory, - $this->reflectionProvider, - $this->dynamicReturnTypeExtensionRegistry, - $this->operatorTypeSpecifyingExtensionRegistry, - $this->printer, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->nodeScopeResolver, - $this->context, - $this->declareStrictTypes, - $this->constantTypes, - $this->function, - $this->namespace, - $this->variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->dynamicConstantNames, - false, - $this->afterExtractCall, - $this->parentScope - ); - } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if ($hasOffsetValue->no()) { + return false; + } - private function promoteNativeTypes(): self - { - $variableTypes = $this->variableTypes; - foreach ($this->nativeExpressionTypes as $expressionType => $type) { - if (substr($expressionType, 0, 1) !== '$') { - throw new \PHPStan\ShouldNotHappenException(); + // 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); + } } - $variableName = substr($expressionType, 1); - $has = $this->hasVariableType($variableName); - if ($has->no()) { - throw new \PHPStan\ShouldNotHappenException(); + // 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; + } + + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if (!$this->hasExpressionType($expr)->yes()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + } + + if ($result !== null) { + return $result; + } + + $result = $typeCallback($propertyReflection->getWritableType()); + if ($result !== null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheck($expr->var, $typeCallback, $result); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheck($expr->class, $typeCallback, $result); + } } - $variableTypes[$variableName] = new VariableTypeHolder($type, $has); + return $result; } - return $this->scopeFactory->create( - $this->context, - $this->declareStrictTypes, - $this->constantTypes, - $this->function, - $this->namespace, - $variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - [] - ); + if ($result !== null) { + return $result; + } + + return $typeCallback($this->getType($expr)); } - /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return bool - */ - private function hasPropertyNativeType($propertyFetch): bool + private function issetCheckUndefined(Expr $expr): ?bool { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $this); - if ($propertyReflection === null) { + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $this->hasVariableType($expr->name); + if (!$hasVariable->no()) { + return null; + } + return false; } - if (!$propertyReflection->isNative()) { + if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $type = $this->getType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $this->issetCheckUndefined($expr->var); + } + + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + + if (!$hasOffsetValue->no()) { + return $this->issetCheckUndefined($expr->var); + } + return false; } - return !$propertyReflection->getNativeType() instanceof MixedType; - } - - /** @api */ - protected function getTypeFromArrayDimFetch( - Expr\ArrayDimFetch $arrayDimFetch, - Type $offsetType, - Type $offsetAccessibleType - ): Type - { - if ($arrayDimFetch->dim === null) { - throw new \PHPStan\ShouldNotHappenException(); + if ($expr instanceof Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); } - if ((new ObjectType(\ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { - return $this->getType( - new MethodCall( - $arrayDimFetch->var, - new Node\Identifier('offsetGet'), - [ - new Node\Arg($arrayDimFetch->dim), - ] - ) - ); + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); } - return $offsetAccessibleType->getOffsetValueType($offsetType); + return null; } - private function calculateFromScalars(Expr $node, ConstantScalarType $leftType, ConstantScalarType $rightType): Type + /** + * @param ParametersAcceptor[] $variants + */ + private function createFirstClassCallable( + FunctionReflection|ExtendedMethodReflection|null $function, + array $variants, + ): Type { - if ($leftType instanceof StringType && $rightType instanceof StringType) { - /** @var string $leftValue */ - $leftValue = $leftType->getValue(); - /** @var string $rightValue */ - $rightValue = $rightType->getValue(); + $closureTypes = []; - if ($node instanceof Expr\BinaryOp\BitwiseAnd || $node instanceof Expr\AssignOp\BitwiseAnd) { - return $this->getTypeFromValue($leftValue & $rightValue); + foreach ($variants as $variant) { + $returnType = $variant->getReturnType(); + if ($variant instanceof ExtendedParametersAcceptor) { + $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType; } - 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); + $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(), + ); } - } - - $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(); - } + $throwPoints = []; + $impurePoints = []; + $acceptsNamedArguments = TrinaryLogic::createYes(); + $mustUseReturnValue = TrinaryLogic::createMaybe(); + if ($variant instanceof CallableParametersAcceptor) { + $throwPoints = $variant->getThrowPoints(); + $impurePoints = $variant->getImpurePoints(); + $acceptsNamedArguments = $variant->acceptsNamedArguments(); + $mustUseReturnValue = $variant->mustUseReturnValue(); + } elseif ($function !== null) { + $returnTypeForThrow = $variant->getReturnType(); + $throwType = $function->getThrowType(); + if ($throwType === null) { + if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new \PHPStan\ShouldNotHappenException(); - } + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } - /** @var float|int $leftNumberValue */ - $leftNumberValue = $leftNumberType->getValue(); + $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant); + if ($impurePoint !== null) { + $impurePoints[] = $impurePoint; + } - /** @var float|int $rightNumberValue */ - $rightNumberValue = $rightNumberType->getValue(); + $acceptsNamedArguments = $function->acceptsNamedArguments(); + $mustUseReturnValue = $function->mustUseReturnValue(); + } - if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) { - return $this->getTypeFromValue($leftNumberValue + $rightNumberValue); + $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: $acceptsNamedArguments, + mustUseReturnValue: $mustUseReturnValue, + ); } - if ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) { - return $this->getTypeFromValue($leftNumberValue - $rightNumberValue); - } + return TypeCombinator::union(...$closureTypes); + } - if ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) { - return $this->getTypeFromValue($leftNumberValue * $rightNumberValue); - } + /** @api */ + public function getNativeType(Expr $expr): Type + { + return $this->promoteNativeTypes()->getType($expr); + } - if ($node instanceof Node\Expr\BinaryOp\Pow || $node instanceof Node\Expr\AssignOp\Pow) { - return $this->getTypeFromValue($leftNumberValue ** $rightNumberValue); - } + public function getKeepVoidType(Expr $node): Type + { + $clonedNode = clone $node; + $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true); - if ($node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) { - return $this->getTypeFromValue($leftNumberValue / $rightNumberValue); - } + return $this->getType($clonedNode); + } - if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Node\Expr\AssignOp\Mod) { - return $this->getTypeFromValue($leftNumberValue % $rightNumberValue); - } + public function doNotTreatPhpDocTypesAsCertain(): Scope + { + return $this->promoteNativeTypes(); + } - if ($node instanceof Expr\BinaryOp\ShiftLeft || $node instanceof Expr\AssignOp\ShiftLeft) { - return $this->getTypeFromValue($leftNumberValue << $rightNumberValue); + private function promoteNativeTypes(): self + { + if ($this->nativeTypesPromoted) { + return $this; } - if ($node instanceof Expr\BinaryOp\ShiftRight || $node instanceof Expr\AssignOp\ShiftRight) { - return $this->getTypeFromValue($leftNumberValue >> $rightNumberValue); + if ($this->scopeWithPromotedNativeTypes !== null) { + return $this->scopeWithPromotedNativeTypes; } - if ($node instanceof Expr\BinaryOp\BitwiseAnd || $node instanceof Expr\AssignOp\BitwiseAnd) { - return $this->getTypeFromValue($leftNumberValue & $rightNumberValue); - } + return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create( + $this->context, + $this->declareStrictTypes, + $this->function, + $this->namespace, + $this->nativeExpressionTypes, + [], + [], + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + true, + ); + } - if ($node instanceof Expr\BinaryOp\BitwiseOr || $node instanceof Expr\AssignOp\BitwiseOr) { - return $this->getTypeFromValue($leftNumberValue | $rightNumberValue); + private function getTypeFromArrayDimFetch( + Expr\ArrayDimFetch $arrayDimFetch, + Type $offsetType, + Type $offsetAccessibleType, + ): Type + { + if ($arrayDimFetch->dim === null) { + throw new ShouldNotHappenException(); } - if ($node instanceof Expr\BinaryOp\BitwiseXor || $node instanceof Expr\AssignOp\BitwiseXor) { - return $this->getTypeFromValue($leftNumberValue ^ $rightNumberValue); + 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 new MixedType(); + return $offsetAccessibleType->getOffsetValueType($offsetType); } - private function resolveExactName(Name $name): ?string + private function resolveExactName(string $name): ?string { - $originalClass = (string) $name; - - switch (strtolower($originalClass)) { + switch (strtolower($name)) { case 'self': if (!$this->isInClass()) { return null; @@ -2596,7 +2887,7 @@ private function resolveExactName(Name $name): ?string return null; } - return $originalClass; + return $name; } /** @api */ @@ -2604,15 +2895,16 @@ public function resolveName(Name $name): string { $originalClass = (string) $name; if ($this->isInClass()) { - if (in_array(strtolower($originalClass), [ + $lowerClass = strtolower($originalClass); + if (in_array($lowerClass, [ 'self', 'static', ], true)) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - return $this->inClosureBindScopeClass; + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + return $this->inClosureBindScopeClasses[0]; } return $this->getClassReflection()->getName(); - } elseif ($originalClass === 'parent') { + } elseif ($lowerClass === 'parent') { $currentClassReflection = $this->getClassReflection(); if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); @@ -2627,9 +2919,9 @@ public function resolveName(Name $name): string public function resolveTypeByName(Name $name): TypeWithClassName { if ($name->toLowerString() === 'static' && $this->isInClass()) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) { + return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0])); } } @@ -2638,11 +2930,11 @@ public function resolveTypeByName(Name $name): TypeWithClassName $originalClass = $this->resolveName($name); if ($this->isInClass()) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static' && $originalClass === $this->getClassReflection()->getName()) { - if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - return new ThisType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + if ($this->inClosureBindScopeClasses === [$originalClass]) { + if ($this->reflectionProvider->hasClass($originalClass)) { + return new ThisType($this->reflectionProvider->getClass($originalClass)); } - return new ObjectType($this->inClosureBindScopeClass); + return new ObjectType($originalClass); } $thisType = new ThisType($this->getClassReflection()); @@ -2655,6 +2947,26 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } + private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName + { + $classType = $this->resolveTypeByName($class); + + if ( + $classType instanceof StaticType + && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true) + ) { + $methodReflectionCandidate = $this->getMethodReflection( + $classType, + $name->name, + ); + if ($methodReflectionCandidate !== null && $methodReflectionCandidate->isStatic()) { + $classType = $classType->getStaticObjectType(); + } + } + + return $classType; + } + /** * @api * @param mixed $value @@ -2665,40 +2977,44 @@ public function getTypeFromValue($value): Type } /** @api */ - public function isSpecified(Expr $node): bool + public function hasExpressionType(Expr $node): TrinaryLogic { - $exprString = $this->getNodeKey($node); + if ($node instanceof Variable && is_string($node->name)) { + return $this->hasVariableType($node->name); + } - return isset($this->moreSpecificTypes[$exprString]) - && $this->moreSpecificTypes[$exprString]->getCertainty()->yes(); + $exprString = $this->getNodeKey($node); + if (!isset($this->expressionTypes[$exprString])) { + return TrinaryLogic::createNo(); + } + return $this->expressionTypes[$exprString]->getCertainty(); } /** - * @param MethodReflection|FunctionReflection $reflection - * @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->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $stack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -2710,27 +3026,27 @@ public function popInFunctionCall(): self return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $stack, $this->afterExtractCall, - $this->parentScope + $this->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; } @@ -2747,53 +3063,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, + $namespace, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, ); } /** * @api - * @param Node\Stmt\ClassMethod $classMethod - * @param TemplateTypeMap $templateTypeMap * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure - * @return self + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2805,33 +3158,138 @@ public function enterClassMethod( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null + ?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), - $this->transformStaticType($this->getFunctionType($classMethod->returnType, $classMethod->returnType === null, false)), - $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, - $throwType, + $this->getParameterAttributes($classMethod), + $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), + $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, + $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, - $isPure + $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() + !$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'), type: $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 !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null, + $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, ); } @@ -2844,7 +3302,7 @@ private function transformStaticType(Type $type): Type if ($type instanceof StaticType) { $classReflection = $this->getClassReflection(); $changedType = $type->changeBaseClass($classReflection); - if ($classReflection->isFinal()) { + if ($classReflection->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); @@ -2855,7 +3313,6 @@ private function transformStaticType(Type $type): Type } /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterTypes(Node\FunctionLike $functionLike): array @@ -2863,12 +3320,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, ); } @@ -2876,7 +3333,6 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array @@ -2887,7 +3343,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); } @@ -2895,19 +3351,33 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): return $realParameterDefaultValues; } + /** + * @return array> + */ + private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array + { + $parameterAttributes = []; + $className = null; + if ($this->isInClass()) { + $className = $this->getClassReflection()->getName(); + } + foreach ($functionLike->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + + $parameterAttributes[$parameter->var->name] = $this->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike)); + } + + return $parameterAttributes; + } + /** * @api - * @param Node\Stmt\Function_ $function - * @param TemplateTypeMap $templateTypeMap * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure - * @return self + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2918,170 +3388,291 @@ public function enterFunction( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, - ?bool $isPure = null + ?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), + $this->getParameterAttributes($function), $this->getFunctionType($function->returnType, $function->returnType === null, false), $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, - $isPure + $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 + false, ); } private function enterFunctionLike( PhpFunctionFromParserNodeReflection $functionReflection, - bool $preserveThis + bool $preserveConstructorScope, ): self { - $variableTypes = []; + $parametersByName = []; + + foreach ($functionReflection->getParameters() as $parameter) { + $parametersByName[$parameter->getName()] = $parameter; + } + + $expressionTypes = []; $nativeExpressionTypes = []; - foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameter) { + $conditionalTypes = []; + + if ($preserveConstructorScope) { + $expressionTypes = $this->rememberConstructorExpressions($this->expressionTypes); + $nativeExpressionTypes = $this->rememberConstructorExpressions($this->nativeExpressionTypes); + } + + foreach ($functionReflection->getParameters() as $parameter) { $parameterType = $parameter->getType(); + + if ($parameterType instanceof ConditionalTypeForParameter) { + $targetParameterName = substr($parameterType->getParameterName(), 1); + if (array_key_exists($targetParameterName, $parametersByName)) { + $targetParameter = $parametersByName[$targetParameterName]; + + $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf(); + $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse(); + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $ifType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $elseType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + } + } + + $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { - $parameterType = new ArrayType(new IntegerType(), $parameterType); + 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()); + } } - $variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameterType); - $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $parameter->getNativeType(); - } + $parameterNode = new Variable($parameter->getName()); + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType); - if ($preserveThis && array_key_exists('this', $this->variableTypes)) { - $variableTypes['this'] = $this->variableTypes['this']; + $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName()); + $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr); + $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType); + + $nativeParameterType = $parameter->getNativeType(); + if ($parameter->isVariadic()) { + 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()); + } + } + $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, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $scopeClass, - $this->anonymousFunctionReflection + $scopeClasses, + $this->anonymousFunctionReflection, ); } public function restoreOriginalScopeAfterClosureBind(self $originalScope): self { - $variableTypes = $this->getVariableTypes(); - if (isset($originalScope->variableTypes['this'])) { - $variableTypes['this'] = $originalScope->variableTypes['this']; + $expressionTypes = $this->expressionTypes; + if (isset($originalScope->expressionTypes['$this'])) { + $expressionTypes['$this'] = $originalScope->expressionTypes['$this']; + } else { + unset($expressionTypes['$this']); + } + + $nativeExpressionTypes = $this->nativeExpressionTypes; + if (isset($originalScope->nativeExpressionTypes['$this'])) { + $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this']; + } else { + unset($nativeExpressionTypes['$this']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $originalScope->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + ); + } + + public function restoreThis(self $restoreThisScope): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + + if ($restoreThisScope->isInClass()) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $nativeExpressionTypes[$exprString] = $expressionTypeHolder; + } } else { - unset($variableTypes['this']); + unset($expressionTypes['$this']); + unset($nativeExpressionTypes['$this']); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $originalScope->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } - public function enterClosureCall(Type $thisType): self + public function enterClosureCall(Type $thisType, Type $nativeThisType): self { - $variableTypes = $this->getVariableTypes(); - $variableTypes['this'] = VariableTypeHolder::createYes($thisType); + $expressionTypes = $this->expressionTypes; + $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType); + + $nativeExpressionTypes = $this->nativeExpressionTypes; + $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $thisType instanceof TypeWithClassName ? $thisType->getClassName() : null, - $this->anonymousFunctionReflection + $thisType->getObjectClassNames(), + $this->anonymousFunctionReflection, ); } /** @api */ public function isInClosureBind(): bool { - return $this->inClosureBindScopeClass !== null; + return $this->inClosureBindScopeClasses !== []; } /** * @api - * @param \PhpParser\Node\Expr\Closure $closure - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters - * @return self + * @param ParameterReflection[]|null $callableParameters */ public function enterAnonymousFunction( Expr\Closure $closure, - ?array $callableParameters = null + ?array $callableParameters, ): self { $anonymousFunctionReflection = $this->getType($closure); if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters); @@ -3089,125 +3680,195 @@ public function enterAnonymousFunction( return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, [], - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], - $scope->nativeExpressionTypes, [], + $this->inFunctionCallsStack, false, - $this + $this, + $this->nativeTypesPromoted, ); } /** - * @param \PhpParser\Node\Expr\Closure $closure - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters - * @return self + * @param ParameterReflection[]|null $callableParameters */ private function enterAnonymousFunctionWithoutReflection( Expr\Closure $closure, - ?array $callableParameters = null + ?array $callableParameters, ): self { - $variableTypes = []; + $expressionTypes = []; + $nativeTypes = []; foreach ($closure->params as $i => $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $paramExprString = sprintf('$%s', $parameter->var->name); $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } - - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes( - $parameterType - ); + $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; } - $nativeTypes = []; - $moreSpecificTypes = []; + $nonRefVariableNames = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { - throw new \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; } - $variableName = $use->var->name; + $nonRefVariableNames[$variableName] = true; if ($this->hasVariableType($variableName)->no()) { $variableType = new ErrorType(); + $variableNativeType = new ErrorType(); } else { $variableType = $this->getVariableType($variableName); - $nativeTypes[sprintf('$%s', $variableName)] = $this->getNativeType($use->var); + $variableNativeType = $this->getNativeType($use->var); } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - foreach ($this->moreSpecificTypes as $exprString => $moreSpecificType) { - $matches = \Nette\Utils\Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($matches === []) { - continue; - } + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType); + } - $matches = array_column($matches, 1); - if (!in_array($variableName, $matches, true)) { - continue; - } + $nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes); + foreach ($nonStaticExpressions as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + + if ($expr instanceof Variable) { + continue; + } - $moreSpecificTypes[$exprString] = $moreSpecificType; + $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; } if ($this->hasVariableType('this')->yes() && !$closure->static) { - $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this')); + $node = new Variable('this'); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node)); + $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node)); + + if ($this->phpVersion->supportsReadOnlyProperties()) { + foreach ($nonStaticExpressions as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + + if (!$expr instanceof PropertyFetch) { + continue; + } + + if (!$this->isReadonlyPropertyFetch($expr, true)) { + continue; + } + + $expressionTypes[$exprString] = $typeHolder; + } + } } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $moreSpecificTypes, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeTypes), [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, new TrivialParametersAcceptor(), true, [], - $nativeTypes, + [], [], false, - $this + $this, + $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 \PHPStan\Reflection\ParameterReflection[]|null $callableParameters + * @param ParameterReflection[]|null $callableParameters */ - public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters = null): self + public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { $anonymousFunctionReflection = $this->getType($arrowFunction); if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters); @@ -3215,34 +3876,32 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, $scope->conditionalExpressions, - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], [], - [], + $this->inFunctionCallsStack, $scope->afterExtractCall, - $scope->parentScope + $scope->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters + * @param ParameterReflection[]|null $callableParameters */ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { - $variableTypes = $this->variableTypes; - $mixed = new MixedType(); - $parameterVariables = []; + $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); @@ -3250,103 +3909,46 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes($parameterType); - $parameterVariables[] = $parameter->var->name; + $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes()); } if ($arrowFunction->static) { - unset($variableTypes['this']); - } - - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - $newHolders = []; - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - if ($exprString === $conditionalExprString) { - continue 2; - } - } - - foreach ($holders as $holder) { - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 3; - } - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } - - $conditionalExpressions[$conditionalExprString] = $newHolders; - } - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - if ($exprString === $conditionalExprString) { - continue; - } - - $newHolders = []; - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 2; - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } - - $conditionalExpressions[$conditionalExprString] = $newHolders; - } + $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); } return $this->scopeFactory->create( - $this->context, + $arrowFunctionScope->context, $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - null, + $arrowFunctionScope->getFunction(), + $arrowFunctionScope->getNamespace(), + $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes), + $arrowFunctionScope->nativeExpressionTypes, + $arrowFunctionScope->conditionalExpressions, + $arrowFunctionScope->inClosureBindScopeClasses, + new TrivialParametersAcceptor(), true, [], [], [], - $this->afterExtractCall, - $this->parentScope + $arrowFunctionScope->afterExtractCall, + $arrowFunctionScope->parentScope, + $this->nativeTypesPromoted, ); } @@ -3361,24 +3963,29 @@ public function isParameterValueNullable(Node\Param $parameter): bool /** * @api - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\ComplexType|null $type - * @param bool $isNullable - * @param bool $isVariadic - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type { if ($isNullable) { return TypeCombinator::addNull( - $this->getFunctionType($type, false, $isVariadic) + $this->getFunctionType($type, false, $isVariadic), ); } if ($isVariadic) { - 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 instanceof Name) { @@ -3396,42 +4003,76 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); } - public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self + private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType()); - $scope->nativeExpressionTypes[sprintf('$%s', $valueName)] = $nativeIterateeType->getIterableValueType(); + if ($nativeType->isSuperTypeOf($inferredType)->no()) { + return $nativeType; + } - if ($keyName !== null) { - $scope = $scope->enterForeachKey($iteratee, $keyName); + $result = TypeCombinator::intersect($nativeType, $inferredType); + if (TypeCombinator::containsNull($nativeType)) { + return TypeCombinator::addNull($result); } - return $scope; + return $result; + } + + public function enterMatch(Expr\Match_ $expr): self + { + if ($expr->cond instanceof Variable) { + return $this; + } + if ($expr->cond instanceof AlwaysRememberedExpr) { + $cond = $expr->cond->expr; + } else { + $cond = $expr->cond; + } + + $type = $this->getType($cond); + $nativeType = $this->getNativeType($cond); + $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType); + $expr->cond = $condExpr; + + return $this->assignExpression($condExpr, $type, $nativeType); } - public function enterForeachKey(Expr $iteratee, string $keyName): self + public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType()); - $scope->nativeExpressionTypes[sprintf('$%s', $keyName)] = $nativeIterateeType->getIterableKeyType(); + $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($originalScope, $iteratee, $keyName); + } return $scope; } - /** - * @param \PhpParser\Node\Name[] $classes - * @param string|null $variableName - * @return self - */ - public function enterCatch(array $classes, ?string $variableName): self + public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self { - $type = TypeCombinator::union(...array_map(static function (\PhpParser\Node\Name $class): ObjectType { - return new ObjectType((string) $class); - }, $classes)); + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $keyName, + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + TrinaryLogic::createYes(), + ); - return $this->enterCatchType($type, $variableName); + if ($iterateeType->isArray()->yes()) { + $scope = $scope->assignExpression( + new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + ); + } + + return $scope; } public function enterCatchType(Type $catchType, ?string $variableName): self @@ -3442,7 +4083,9 @@ public function enterCatchType(Type $catchType, ?string $variableName): self return $this->assignVariable( $variableName, - TypeCombinator::intersect($catchType, new ObjectType(\Throwable::class)) + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TrinaryLogic::createYes(), ); } @@ -3452,24 +4095,29 @@ public function enterExpressionAssign(Expr $expr): self $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; $currentlyAssignedExpressions[$exprString] = true; - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + + return $scope; } public function exitExpressionAssign(Expr $expr): self @@ -3478,24 +4126,29 @@ public function exitExpressionAssign(Expr $expr): self $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; unset($currentlyAssignedExpressions[$exprString]); - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + + return $scope; } /** @api */ @@ -3505,284 +4158,309 @@ public function isInExpressionAssign(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAssignedExpressions); } - public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $certainty = null): self + public function setAllowedUndefinedExpression(Expr $expr): self { - if ($certainty === null) { - $certainty = TrinaryLogic::createYes(); - } elseif ($certainty->no()) { - throw new \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; + $exprString = $this->getNodeKey($expr); + $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; + $currentlyAllowedUndefinedExpressions[$exprString] = true; + + $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; + + return $scope; + } + + public function unsetAllowedUndefinedExpression(Expr $expr): self + { + $exprString = $this->getNodeKey($expr); + $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; + unset($currentlyAllowedUndefinedExpressions[$exprString]); - $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]*#'); + $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; + + 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 + { + $scope = $this; + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $exprVarType = $scope->getType($expr->var); + $dimType = $scope->getType($expr->dim); + $unsetType = $exprVarType->unsetOffset($dimType); + $exprVarNativeType = $scope->getNativeType($expr->var); + $dimNativeType = $scope->getNativeType($expr->dim); + $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType); + $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression( + new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('count'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('sizeof'), [new Arg($expr->var)]), + ); - if ($matches === []) { - continue; + 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), + ), + ); } + } - $matches = array_column($matches, 0); + return $scope->invalidateExpression($expr); + } - if (!in_array($variableString, $matches, true)) { - continue; + public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self + { + if ($expr instanceof ConstFetch) { + $loweredConstName = strtolower($expr->name->toString()); + if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { + return $this; } - - unset($moreSpecificTypeHolders[$key]); } - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $exprString => $holders) { - $exprVariableName = '$' . $variableName; - if ($exprString === $exprVariableName) { - continue; + 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; } + } - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionExprString) { - if ($conditionExprString === $exprVariableName) { - continue 3; + $scope = $this; + if ( + $expr instanceof Expr\ArrayDimFetch + && $expr->dim !== null + && !$expr->dim instanceof Expr\PreInc + && !$expr->dim instanceof Expr\PreDec + && !$expr->dim instanceof Expr\PostDec + && !$expr->dim instanceof Expr\PostInc + ) { + $dimType = $scope->getType($expr->dim)->toArrayKey(); + if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) { + $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->isInteger()->yes()) { + $types[] = new StringType(); + } + $offsetValueType = TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)); + + if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { + $offsetValueType = TypeCombinator::intersect( + $offsetValueType, + new HasOffsetValueType($dimType, $type), + ); } + + $scope = $scope->specifyExpressionType( + $expr->var, + $offsetValueType, + $scope->getNativeType($expr->var), + $certainty, + ); } } + } - $conditionalExpressions[$exprString] = $holders; + if ($certainty->no()) { + throw new ShouldNotHappenException(); } - return $this->scopeFactory->create( + $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->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $moreSpecificTypeHolders, - $conditionalExpressions, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $nativeTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); - } - public function unsetExpression(Expr $expr): self - { - if ($expr instanceof Variable && is_string($expr->name)) { - if ($this->hasVariableType($expr->name)->no()) { - return $this; - } - $variableTypes = $this->getVariableTypes(); - unset($variableTypes[$expr->name]); - $nativeTypes = $this->nativeExpressionTypes; - - $exprString = sprintf('$%s', $expr->name); - unset($nativeTypes[$exprString]); - - $conditionalExpressions = $this->conditionalExpressions; - unset($conditionalExpressions[$exprString]); - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - [], - $nativeTypes, - [], - $this->afterExtractCall, - $this->parentScope - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $varType = $this->getType($expr->var); - $constantArrays = TypeUtils::getConstantArrays($varType); - if (count($constantArrays) > 0) { - $unsetArrays = []; - $dimType = $this->getType($expr->dim); - foreach ($constantArrays as $constantArray) { - $unsetArrays[] = $constantArray->unsetOffset($dimType); - } - return $this->specifyExpressionType( - $expr->var, - TypeCombinator::union(...$unsetArrays) - ); - } + if ($expr instanceof AlwaysRememberedExpr) { + return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); + } - $arrays = TypeUtils::getArrays($varType); - $scope = $this; - if (count($arrays) > 0) { - $scope = $scope->specifyExpressionType($expr->var, TypeCombinator::union(...$arrays)); - } + return $scope; + } - return $scope->invalidateExpression($expr->var); + public function assignExpression(Expr $expr, Type $type, Type $nativeType): self + { + $scope = $this; + if ($expr instanceof PropertyFetch) { + $scope = $this->invalidateExpression($expr) + ->invalidateMethodsOnExpression($expr->var); + } elseif ($expr instanceof Expr\StaticPropertyFetch) { + $scope = $this->invalidateExpression($expr); + } elseif ($expr instanceof Variable) { + $scope = $this->invalidateExpression($expr); } - return $this; + return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes()); } - public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType = null): self + public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self { - if ($expr instanceof Node\Scalar || $expr instanceof Array_) { + if (!$this->isInClass()) { return $this; } - if ($expr instanceof ConstFetch) { - $constantTypes = $this->constantTypes; - $constantName = new FullyQualified($expr->name->toString()); - $constantTypes[$constantName->toCodeString()] = $type; - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope - ); + if (TypeUtils::findThisType($fetchedOnType) === null) { + return $this; } - $exprString = $this->getNodeKey($expr); - - $scope = $this; - - if ($expr instanceof Variable && is_string($expr->name)) { - $variableName = $expr->name; - - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = VariableTypeHolder::createYes($type); - - if ($nativeType === null) { - $nativeType = $type; - } - - $nativeTypes = $this->nativeExpressionTypes; - $exprString = sprintf('$%s', $variableName); - $nativeTypes[$exprString] = $nativeType; - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $nativeTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); - if (count($constantArrays) > 0) { - $setArrays = []; - $dimType = $this->getType($expr->dim); - foreach ($constantArrays as $constantArray) { - $setArrays[] = $constantArray->setOffsetValueType($dimType, $type); - } - $scope = $this->specifyExpressionType( - $expr->var, - TypeCombinator::union(...$setArrays) - ); - } + $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + return $this; } - - if ($expr instanceof FuncCall && $expr->name instanceof Name && $type instanceof ConstantBooleanType && !$type->getValue()) { - $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this); - if ($functionName !== null && in_array(strtolower($functionName), [ - 'is_dir', - 'is_file', - 'file_exists', - ], true)) { - return $this; - } + $declaringClass = $propertyReflection->getDeclaringClass(); + if ($this->getClassReflection()->getName() !== $declaringClass->getName()) { + return $this; } - - return $scope->addMoreSpecificTypes([ - $exprString => $type, - ]); - } - - public function assignExpression(Expr $expr, Type $type): self - { - $scope = $this; - if ($expr instanceof PropertyFetch) { - $scope = $this->invalidateExpression($expr) - ->invalidateMethodsOnExpression($expr->var); - } elseif ($expr instanceof Expr\StaticPropertyFetch) { - $scope = $this->invalidateExpression($expr); + if (!$declaringClass->hasNativeProperty($propertyName)) { + return $this; } - return $scope->specifyExpressionType($expr, $type); + return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { - $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $expressionToInvalidateClass = get_class($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; - $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - try { - $expr = $this->parser->parseString(' $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $requireMoreCharacters)) { continue; } - if (!$expr instanceof Node\Stmt\Expression) { - throw new \PHPStan\ShouldNotHappenException(); - } - $found = $nodeFinder->findFirst([$expr->expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { - if (!$node instanceof $expressionToInvalidateClass) { - return false; - } - return $this->getNodeKey($node) === $exprStringToInvalidate; - }); - if ($found === null) { + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + $newConditionalExpressions = []; + foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { + if (count($holders) === 0) { continue; } - - if ($requireMoreCharacters && $exprString === $exprStringToInvalidate) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $holders[array_key_first($holders)]->getTypeHolder()->getExpr())) { + $invalidated = true; continue; } - - unset($moreSpecificTypeHolders[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; + foreach ($holders as $holder) { + $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders(); + foreach ($conditionalTypeHolders as $conditionalTypeHolder) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr())) { + $invalidated = true; + continue 3; + } + } + } + $newConditionalExpressions[$conditionalExprString] = $holders; } if (!$invalidated) { @@ -3792,42 +4470,82 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeExpressionTypes, + $newConditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } - public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self + 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 ( + $expr instanceof PropertyFetch + && $requireMoreCharacters + && $this->isReadonlyPropertyFetch($expr, false) + ) { + return false; + } + + return true; + } + + private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self { $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; - - try { - $expr = $this->parser->parseString('findFirst([$expr->expr], function (Node $node) use ($exprStringToInvalidate): bool { + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($exprStringToInvalidate): bool { if (!$node instanceof MethodCall) { return false; } @@ -3838,7 +4556,7 @@ public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): sel continue; } - unset($moreSpecificTypeHolders[$exprString]); + unset($expressionTypes[$exprString]); unset($nativeExpressionTypes[$exprString]); $invalidated = true; } @@ -3850,231 +4568,237 @@ public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): sel return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, - $this->parentScope + $this->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; } /** * @api - * @param \PhpParser\Node\Expr $expr - * @return \PHPStan\Analyser\MutatingScope + * @return MutatingScope */ public function filterByTruthyValue(Expr $expr): Scope { + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->truthyScopes)) { + return $this->truthyScopes[$exprString]; + } + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy()); - return $this->filterBySpecifiedTypes($specifiedTypes); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->truthyScopes[$exprString] = $scope; + + return $scope; } /** * @api - * @param \PhpParser\Node\Expr $expr - * @return \PHPStan\Analyser\MutatingScope + * @return MutatingScope */ public function filterByFalseyValue(Expr $expr): Scope { + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->falseyScopes)) { + return $this->falseyScopes[$exprString]; + } + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey()); - return $this->filterBySpecifiedTypes($specifiedTypes); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->falseyScopes[$exprString] = $scope; + + return $scope; } public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { $typeSpecifications = []; foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => true, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => false, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } usort($typeSpecifications, static function (array $a, array $b): int { - // @phpstan-ignore-next-line - $length = strlen((string) $a['exprString']) - strlen((string) $b['exprString']); + $length = strlen($a['exprString']) - strlen($b['exprString']); if ($length !== 0) { return $length; } - return $b['sure'] - $a['sure']; // @phpstan-ignore-line + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric }); $scope = $this; - $typeGuards = []; - $skipVariables = []; - $saveConditionalVariables = []; + $specifiedExpressions = []; foreach ($typeSpecifications as $typeSpecification) { $expr = $typeSpecification['expr']; $type = $typeSpecification['type']; - $originalExprType = $this->getType($expr); - if ($typeSpecification['sure']) { - $scope = $scope->specifyExpressionType($expr, $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $originalExprType)); - if ($expr instanceof Variable && is_string($expr->name)) { - $scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getNativeType($expr)); - } - } else { - $scope = $scope->removeTypeFromExpression($expr, $type); - } + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); - if ( - !$expr instanceof Variable - || !is_string($expr->name) - || $specifiedTypes->shouldOverwrite() - ) { - // @phpstan-ignore-next-line - $match = \Nette\Utils\Strings::match((string) $typeSpecification['exprString'], '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($match !== null) { - $skipVariables[$match[1]] = true; + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); } - continue; - } - if ($scope->hasVariableType($expr->name)->no()) { continue; } - $saveConditionalVariables[$expr->name] = $scope->getVariableType($expr->name); - } - - foreach ($saveConditionalVariables as $variableName => $typeGuard) { - if (array_key_exists($variableName, $skipVariables)) { - continue; + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $scope = $scope->addTypeToExpression($expr, $type); + } + } else { + $scope = $scope->removeTypeFromExpression($expr, $type); } - - $typeGuards['$' . $variableName] = $typeGuard; + $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr)); } - $newConditionalExpressions = $specifiedTypes->getNewConditionalExpressionHolders(); - foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) { - if (array_key_exists($variableExprString, $typeGuards)) { - continue; - } - - $typeHolder = null; - - $variableName = substr($variableExprString, 1); + $conditions = []; + foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { foreach ($conditionalExpressions as $conditionalExpression) { - $matchingConditions = []; - foreach ($conditionalExpression->getConditionExpressionTypes() as $conditionExprString => $conditionalType) { - if (!array_key_exists($conditionExprString, $typeGuards)) { - continue; - } - - if (!$typeGuards[$conditionExprString]->equals($conditionalType)) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { continue 2; } - - $matchingConditions[$conditionExprString] = $conditionalType; - } - - if (count($matchingConditions) === 0) { - $newConditionalExpressions[$variableExprString][$conditionalExpression->getKey()] = $conditionalExpression; - continue; - } - - if (count($matchingConditions) < count($conditionalExpression->getConditionExpressionTypes())) { - $filteredConditions = $conditionalExpression->getConditionExpressionTypes(); - foreach (array_keys($matchingConditions) as $conditionExprString) { - unset($filteredConditions[$conditionExprString]); - } - - $holder = new ConditionalExpressionHolder($filteredConditions, $conditionalExpression->getTypeHolder()); - $newConditionalExpressions[$variableExprString][$holder->getKey()] = $holder; - continue; } - $typeHolder = $conditionalExpression->getTypeHolder(); - break; - } - - if ($typeHolder === null) { - continue; + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } + } - if ($typeHolder->getCertainty()->no()) { - unset($scope->variableTypes[$variableName]); + foreach ($conditions as $conditionalExprString => $expressions) { + $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); + if ($certainty->no()) { + unset($scope->expressionTypes[$conditionalExprString]); } else { - $scope->variableTypes[$variableName] = $typeHolder; + $type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions)); + + $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes) + ? new ExpressionTypeHolder( + $scope->expressionTypes[$conditionalExprString]->getExpr(), + TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type), + TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty), + ) + : $expressions[0]->getTypeHolder(); } } - return $scope->changeConditionalExpressions($newConditionalExpressions); - } - - /** - * @param array $newConditionalExpressionHolders - * @return self - */ - public function changeConditionalExpressions(array $newConditionalExpressionHolders): self - { - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, - $newConditionalExpressionHolders, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, ); } /** - * @param string $exprString * @param ConditionalExpressionHolder[] $conditionalExpressionHolders - * @return self */ public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self { @@ -4083,43 +4807,57 @@ public function addConditionalExpressions(string $exprString, array $conditional return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } public function exitFirstLevelStatements(): self { - return $this->scopeFactory->create( + if (!$this->inFirstLevelStatement) { + return $this; + } + + if ($this->scopeOutOfFirstLevelStatement !== null) { + return $this->scopeOutOfFirstLevelStatement; + } + + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, false, $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + $this->scopeOutOfFirstLevelStatement = $scope; + + return $scope; } /** @api */ @@ -4128,112 +4866,45 @@ public function isInFirstLevelStatement(): bool return $this->inFirstLevelStatement; } - /** - * @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); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - [], - $this->afterExtractCall, - $this->parentScope - ); - } - public function mergeWith(?self $otherScope): self { if ($otherScope === null) { return $this; } + $ourExpressionTypes = $this->expressionTypes; + $theirExpressionTypes = $otherScope->expressionTypes; - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; - - $ourVariableTypes = $this->getVariableTypes(); - $theirVariableTypes = $otherScope->getVariableTypes(); - if ($this->canAnyVariableExist()) { - foreach (array_keys($theirVariableTypes) as $name) { - if (array_key_exists($name, $ourVariableTypes)) { - continue; - } - - $ourVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - - foreach (array_keys($ourVariableTypes) as $name) { - if (array_key_exists($name, $theirVariableTypes)) { - continue; - } - - $theirVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - } - - $mergedVariableHolders = $this->mergeVariableHolders($ourVariableTypes, $theirVariableTypes); + $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $ourVariableTypes, - $theirVariableTypes, - $mergedVariableHolders + $ourExpressionTypes, + $theirExpressionTypes, + $mergedExpressionTypes, ); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $theirVariableTypes, - $ourVariableTypes, - $mergedVariableHolders + $theirExpressionTypes, + $ourExpressionTypes, + $mergedExpressionTypes, ); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), - $mergedVariableHolders, - $this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes), + $mergedExpressionTypes, + $this->mergeVariableHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes), $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - ), $filterVariableHolders)), + [], [], $this->afterExtractCall && $otherScope->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -4264,59 +4935,58 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi /** * @param array $conditionalExpressions - * @param array $variableTypes - * @param array $theirVariableTypes - * @param array $mergedVariableHolders + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $mergedExpressionTypes * @return array */ private function createConditionalExpressions( array $conditionalExpressions, - array $variableTypes, - array $theirVariableTypes, - array $mergedVariableHolders + array $ourExpressionTypes, + array $theirExpressionTypes, + array $mergedExpressionTypes, ): array { - $newVariableTypes = $variableTypes; - foreach ($theirVariableTypes as $name => $holder) { - if (!array_key_exists($name, $mergedVariableHolders)) { + $newVariableTypes = $ourExpressionTypes; + foreach ($theirExpressionTypes as $exprString => $holder) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if (!$mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if (!$mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - unset($newVariableTypes[$name]); + unset($newVariableTypes[$exprString]); } $typeGuards = []; - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if (!$holder->getCertainty()->yes()) { continue; } - if (!array_key_exists($name, $mergedVariableHolders)) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if ($mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if ($mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - $typeGuards['$' . $name] = $holder->getType(); + $typeGuards[$exprString] = $holder; } if (count($typeGuards) === 0) { return $conditionalExpressions; } - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if ( - array_key_exists($name, $mergedVariableHolders) - && $mergedVariableHolders[$name]->equals($holder) + array_key_exists($exprString, $mergedExpressionTypes) + && $mergedExpressionTypes[$exprString]->equals($holder) ) { continue; } - $exprString = '$' . $name; $variableTypeGuards = $typeGuards; unset($variableTypeGuards[$exprString]); @@ -4328,152 +4998,178 @@ private function createConditionalExpressions( $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } - foreach (array_keys($mergedVariableHolders) as $name) { - if (array_key_exists($name, $variableTypes)) { + foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { + if (array_key_exists($exprString, $ourExpressionTypes)) { continue; } - $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new VariableTypeHolder(new ErrorType(), TrinaryLogic::createNo())); - $conditionalExpressions['$' . $name][$conditionalExpression->getKey()] = $conditionalExpression; + $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } return $conditionalExpressions; } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $theirVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $ourVariableTypeHolders + * @param array $theirVariableTypeHolders + * @return array */ private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array { $intersectedVariableTypeHolders = []; - foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($theirVariableTypeHolders[$name])) { - $intersectedVariableTypeHolders[$name] = $variableTypeHolder->and($theirVariableTypeHolders[$name]); + $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name); + $nodeFinder = new NodeFinder(); + foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($theirVariableTypeHolders[$exprString])) { + if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) { + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder; + continue; + } + + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]); } else { - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { + continue; + } + + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } } - foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($intersectedVariableTypeHolders[$name])) { + foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($intersectedVariableTypeHolders[$exprString])) { + continue; + } + + $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()); - }; - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; + $scope = $this; + foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) { + $exprString = (string) $exprString; + if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) { + continue; + } + $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1); + $propertyExpr = new PropertyInitializationExpr($propertyName); + if (!array_key_exists($exprString, $scope->expressionTypes)) { + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = $typeHolder; + continue; + } + + $certainty = $scope->expressionTypes[$exprString]->getCertainty(); + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder( + $typeHolder->getExpr(), + $typeHolder->getType(), + $typeHolder->getCertainty()->or($certainty), + ); + } + + return $scope; + } + public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + { return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $finallyScope->constantTypes), - array_map($typeToVariableHolder, $originalFinallyScope->constantTypes) - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $this->processFinallyScopeVariableTypeHolders( - $this->getVariableTypes(), - $finallyScope->getVariableTypes(), - $originalFinallyScope->getVariableTypes() + $this->expressionTypes, + $finallyScope->expressionTypes, + $originalFinallyScope->expressionTypes, ), $this->processFinallyScopeVariableTypeHolders( - $this->moreSpecificTypes, - $finallyScope->moreSpecificTypes, - $originalFinallyScope->moreSpecificTypes + $this->nativeExpressionTypes, + $finallyScope->nativeExpressionTypes, + $originalFinallyScope->nativeExpressionTypes, ), $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $finallyScope->nativeExpressionTypes), - array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes) - ), $filterVariableHolders)), + [], [], $this->afterExtractCall, - $this->parentScope + $this->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 { $nativeExpressionTypes = $this->nativeExpressionTypes; - $variableTypes = $this->variableTypes; + $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()); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = new NullType(); + $holder = ExpressionTypeHolder::createYes($use->var, new NullType()); + $expressionTypes[$variableExprString] = $holder; + $nativeExpressionTypes[$variableExprString] = $holder; continue; } @@ -4483,158 +5179,158 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType); + $variableType = $this->generalizeType($variableType, $prevVariableType, 0); } } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = $variableType; + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeExpressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeExpressionTypes, + [], $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->nativeTypesPromoted, ); } public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self { - $variableTypeHolders = $this->variableTypes; - $nativeTypes = $this->nativeExpressionTypes; - foreach ($finalScope->variableTypes as $name => $variableTypeHolder) { - $nativeTypes[sprintf('$%s', $name)] = $variableTypeHolder->getType(); - if (!isset($variableTypeHolders[$name])) { - $variableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expressionTypes = $this->expressionTypes; + foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($expressionTypes[$variableExprString])) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( + $expressionTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()) + $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), ); } - - $moreSpecificTypes = $this->moreSpecificTypes; - foreach ($finalScope->moreSpecificTypes as $exprString => $variableTypeHolder) { - if (!isset($moreSpecificTypes[$exprString])) { - $moreSpecificTypes[$exprString] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $nativeTypes = $this->nativeExpressionTypes; + foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($nativeTypes[$variableExprString])) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $moreSpecificTypes[$exprString] = new VariableTypeHolder( + $nativeTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()) + $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), ); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypeHolders, - $moreSpecificTypes, + $expressionTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeTypes, + [], [], $this->afterExtractCall, - $this->parentScope + $this->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()); - }; - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; - $nativeTypes = array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - ), $filterVariableHolders)); - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $variableTypeHolders, - $moreSpecificTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeTypes, + [], [], $this->afterExtractCall, - $this->parentScope + $this->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])) { + uksort($variableTypeHolders, static fn (string $exprA, string $exprB): int => strlen($exprA) <=> strlen($exprB)); + + $generalizedExpressions = []; + $newVariableTypeHolders = []; + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + foreach ($generalizedExpressions as $generalizedExprString => $generalizedExpr) { + if (!$this->shouldInvalidateExpression($generalizedExprString, $generalizedExpr, $variableTypeHolder->getExpr())) { + continue; + } + + continue 2; + } + if (!isset($otherVariableTypeHolders[$variableExprString])) { + $newVariableTypeHolders[$variableExprString] = $variableTypeHolder; continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( - self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$name]->getType()), - $variableTypeHolder->getCertainty() + $generalizedType = $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0); + if ( + !$generalizedType->equals($variableTypeHolder->getType()) + ) { + $generalizedExpressions[$variableExprString] = $variableTypeHolder->getExpr(); + } + $newVariableTypeHolders[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $generalizedType, + $variableTypeHolder->getCertainty(), ); } - return $variableTypeHolders; + return $newVariableTypeHolders; } - private static function generalizeType(Type $a, Type $b): Type + private function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -4646,6 +5342,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings = ['a' => [], 'b' => []]; $constantArrays = ['a' => [], 'b' => []]; $generalArrays = ['a' => [], 'b' => []]; + $integerRanges = ['a' => [], 'b' => []]; $otherTypes = []; foreach ([ @@ -4669,7 +5366,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings[$key][] = $type; continue; } - if ($type instanceof ConstantArrayType) { + if ($type->isConstantArray()->yes()) { $constantArrays[$key][] = $type; continue; } @@ -4677,6 +5374,10 @@ private static function generalizeType(Type $a, Type $b): Type $generalArrays[$key][] = $type; continue; } + if ($type instanceof IntegerRangeType) { + $integerRanges[$key][] = $type; + continue; + } $otherTypes[] = $type; } @@ -4684,15 +5385,16 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes = []; foreach ([ - $constantIntegers, $constantFloats, $constantBooleans, $constantStrings, ] as $constantTypes) { if (count($constantTypes['a']) === 0) { + if (count($constantTypes['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantTypes['b']); + } continue; - } - if (count($constantTypes['b']) === 0) { + } elseif (count($constantTypes['b']) === 0) { $resultTypes[] = TypeCombinator::union(...$constantTypes['a']); continue; } @@ -4704,7 +5406,7 @@ private static function generalizeType(Type $a, Type $b): Type continue; } - $resultTypes[] = TypeUtils::generalizeType($constantTypes['a'][0], GeneralizePrecision::moreSpecific()); + $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific()); } if (count($constantArrays['a']) > 0) { @@ -4713,26 +5415,44 @@ private static function generalizeType(Type $a, Type $b): Type } else { $constantArraysA = TypeCombinator::union(...$constantArrays['a']); $constantArraysB = TypeCombinator::union(...$constantArrays['b']); - if ($constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())) { + if ( + $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType()) + && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes() + ) { $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) { $resultArrayBuilder->setOffsetValueType( $keyType, - self::generalizeType( + $this->generalizeType( $constantArraysA->getOffsetValueType($keyType), - $constantArraysB->getOffsetValueType($keyType) - ) + $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) { @@ -4744,16 +5464,14 @@ private static function generalizeType(Type $a, Type $b): Type $aValueType = $generalArraysA->getIterableValueType(); $bValueType = $generalArraysB->getIterableValueType(); - $aArrays = TypeUtils::getAnyArrays($aValueType); - $bArrays = TypeUtils::getAnyArrays($bValueType); if ( - count($aArrays) === 1 - && !$aArrays[0] instanceof ConstantArrayType - && count($bArrays) === 1 - && !$bArrays[0] instanceof ConstantArrayType + $aValueType->isArray()->yes() + && $aValueType->isConstantArray()->no() + && $bValueType->isArray()->yes() + && $bValueType->isConstantArray()->no() ) { - $aDepth = self::getArrayDepth($aArrays[0]); - $bDepth = self::getArrayDepth($bArrays[0]); + $aDepth = self::getArrayDepth($aValueType) + $depth; + $bDepth = self::getArrayDepth($bValueType) + $depth; if ( ($aDepth > 2 || $bDepth > 2) && abs($aDepth - $bDepth) > 0 @@ -4763,27 +5481,175 @@ private static function generalizeType(Type $a, Type $b): Type } } - $resultTypes[] = new ArrayType( - TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType)) + $resultType = new ArrayType( + TypeCombinator::union($this->generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union($this->generalizeType($aValueType, $bValueType, $depth + 1)), ); + if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); + } + if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new OversizedArrayType()); + } + $resultTypes[] = $resultType; + } + } elseif (count($generalArrays['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$generalArrays['b']); + } + + 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 static function getArrayDepth(ArrayType $type): int + private static function getArrayDepth(Type $type): int { $depth = 0; - while ($type instanceof ArrayType) { + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); + while (count($arrays) > 0) { $temp = $type->getIterableValueType(); - $arrays = TypeUtils::getAnyArrays($temp); - if (count($arrays) === 1) { - $type = $arrays[0]; - } else { - $type = $temp; - } + $type = $temp; + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); $depth++; } @@ -4796,68 +5662,115 @@ public function equals(self $otherScope): bool return false; } - if (!$this->compareVariableTypeHolders($this->variableTypes, $otherScope->variableTypes)) { - return false; - } - - if (!$this->compareVariableTypeHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes)) { - return false; - } - - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - - $nativeExpressionTypesResult = $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - ); - - if (!$nativeExpressionTypesResult) { + if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) { return false; } - - return $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - ); + return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes); } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders - * @return bool + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders */ private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool { if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) { return false; } - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { return false; } - if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$name]->getCertainty())) { + if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) { return false; } - if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$name]->getType())) { + if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$variableExprString]->getType())) { return false; } - unset($otherVariableTypeHolders[$name]); + unset($otherVariableTypeHolders[$variableExprString]); } return true; } - /** @api */ + private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int + { + while ( + $expr instanceof BinaryOp\BooleanOr + || $expr instanceof BinaryOp\LogicalOr + || $expr instanceof BinaryOp\BooleanAnd + || $expr instanceof BinaryOp\LogicalAnd + ) { + return $this->getBooleanExpressionDepth($expr->left, $depth + 1); + } + + return $depth; + } + + /** + * @api + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool { return $this->canAccessClassMember($propertyReflection); } + /** @api */ + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $this->canAccessClassMember($propertyReflection); + } + + /** @api */ + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) { + return $this->canAccessClassMember($propertyReflection); + } + + if (!$this->phpVersion->supportsAsymmetricVisibility()) { + return $this->canAccessClassMember($propertyReflection); + } + + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) { + if ($propertyReflection->isPrivateSet()) { + return $classReflection->getName() === $propertyDeclaringClass->getName(); + } + + // protected set + + if ( + $classReflection->getName() === $propertyDeclaringClass->getName() + || $classReflection->isSubclassOfClass($propertyDeclaringClass->removeFinalKeywordOverride()) + ) { + return true; + } + + return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } + + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } + } + + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); + } + + return false; + } + /** @api */ public function canCallMethod(MethodReflection $methodReflection): bool { @@ -4869,7 +5782,7 @@ public function canCallMethod(MethodReflection $methodReflection): bool } /** @api */ - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $this->canAccessClassMember($constantReflection); } @@ -4880,29 +5793,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(); + } + + // protected + + if ( + $classReflection->getName() === $classMemberDeclaringClass->getName() + || $classReflection->isSubclassOfClass($classMemberDeclaringClass->removeFinalKeywordOverride()) + ) { + return true; + } + + return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } - $classReflectionName = $classMemberReflection->getDeclaringClass()->getName(); - if ($classMemberReflection->isPrivate()) { - return $currentClassReflection->getName() === $classReflectionName; + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } } - // protected - - if ( - $currentClassReflection->getName() === $classReflectionName - || $currentClassReflection->isSubclassOf($classReflectionName) - ) { - return true; + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); } - return $classMemberReflection->getDeclaringClass()->isSubclassOf($currentClassReflection->getName()); + return false; } /** @@ -4911,35 +5834,53 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti public function debug(): array { $descriptions = []; - foreach ($this->getVariableTypes() as $name => $variableTypeHolder) { - $key = sprintf('$%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); + foreach ($this->expressionTypes as $name => $variableTypeHolder) { + $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->moreSpecificTypes as $exprString => $typeHolder) { - $key = sprintf( - '%s-specified (%s)', - $exprString, - $typeHolder->getCertainty()->describe() - ); - $descriptions[$key] = $typeHolder->getType()->describe(VerbosityLevel::precise()); - } - foreach ($this->constantTypes as $name => $type) { - $key = sprintf('const %s', $name); - $descriptions[$key] = $type->describe(VerbosityLevel::precise()); + foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) { + $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe()); + $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->nativeExpressionTypes as $exprString => $nativeType) { - $key = sprintf('native %s', $exprString); - $descriptions[$key] = $nativeType->describe(VerbosityLevel::precise()); + + foreach ($this->conditionalExpressions as $exprString => $holders) { + foreach (array_values($holders) as $i => $holder) { + $key = sprintf('condition about %s #%d', $exprString, $i + 1); + $parts = []; + foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) { + $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise()); + } + $condition = implode(' && ', $parts); + $descriptions[$key] = sprintf( + 'if %s then %s is %s (%s)', + $condition, + $exprString, + $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()), + $holder->getTypeHolder()->getCertainty()->describe(), + ); + } } return $descriptions; } + /** + * @param non-empty-string $className + */ private function exactInstantiation(New_ $node, string $className): ?Type { - $resolvedClassName = $this->resolveExactName(new Name($className)); + $resolvedClassName = $this->resolveExactName($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)) { @@ -4947,25 +5888,52 @@ 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->getArgs() + $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) { @@ -4977,147 +5945,294 @@ private function exactInstantiation(New_ $node, string $className): ?Type return $methodResult; } - $objectType = new ObjectType($resolvedClassName); + $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName, classReflection: $classReflection); if (!$classReflection->isGeneric()) { return $objectType; } - $parentNode = $node->getAttribute('parent'); - if ( - ( - $parentNode instanceof Expr\Assign - || $parentNode instanceof Expr\AssignRef - ) - && $parentNode->var instanceof PropertyFetch - ) { - $constructorVariant = ParametersAcceptorSelector::selectSingle($constructorMethod->getVariants()); - $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()]); + $assignedToProperty = $node->getAttribute(NewAssignedToPropertyVisitor::ATTRIBUTE_NAME); + if ($assignedToProperty !== null) { + $constructorVariants = $constructorMethod->getVariants(); + if (count($constructorVariants) === 1) { + $constructorVariant = $constructorVariants[0]; + $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 $type; + + return $traverse($type); + }); + } + + if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { + $propertyType = TypeCombinator::removeNull($this->getType($assignedToProperty)); + $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); + if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { + return $propertyType; } + } + } + } - return $traverse($type); - }); + 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, + classReflection: $classReflection->withTypes($types)->asFinal(), + ); + } + + 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, + classReflection: $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, + [], + ); + } - if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { - $propertyType = $this->getType($parentNode->var); - if ($objectType->isSuperTypeOf($propertyType)->yes()) { - return $propertyType; + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + classReflection: $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, + classReflection: $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, + classReflection: $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, + [], + ); } - } - if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()) + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), ); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $methodCall->getArgs(), - $constructorMethod->getVariants() + $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), ); - return new GenericObjectType( + $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + $newGenericType = new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()) + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), ); - } - - private function getTypeToInstantiateForNew(Type $type): Type - { - $decideType = static function (Type $type): ?Type { - if ($type instanceof ConstantStringType) { - return new ObjectType($type->getValue()); - } - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { - return $type; - } - return null; - }; - - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $decidedType = $decideType($innerType); - if ($decidedType === null) { - return new ObjectWithoutClassType(); + if ($isStatic) { + $newGenericType = new GenericStaticType( + $classReflection, + $types, + null, + [], + ); + } + return TypeTraverser::map($newGenericType, static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type { + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $type->getDefault() ?? $type->getBound(); } - $types[] = $decidedType; + return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); } - return TypeCombinator::union(...$types); + return $traverse($type); + }); + } + + private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type + { + if ($typeWithMethod instanceof UnionType) { + $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes()); } - $decidedType = $decideType($type); - if ($decidedType === null) { - return new ObjectWithoutClassType(); + if (!$typeWithMethod->hasMethod($methodName)->yes()) { + return null; } - return $decidedType; + return $typeWithMethod; } /** @api */ - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection { - if ($typeWithMethod instanceof UnionType) { - $newTypes = []; - foreach ($typeWithMethod->getTypes() as $innerType) { - if (!$innerType->hasMethod($methodName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithMethod = TypeCombinator::union(...$newTypes); + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; } - if (!$typeWithMethod->hasMethod($methodName)->yes()) { + return $type->getMethod($methodName, $this); + } + + public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { return null; } - return $typeWithMethod->getMethod($methodName, $this); + return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod(); } /** - * @param \PHPStan\Type\Type $typeWithMethod - * @param string $methodName - * @param MethodCall|\PhpParser\Node\Expr\StaticCall $methodCall - * @return \PHPStan\Type\Type|null + * @param MethodCall|Node\Expr\StaticCall $methodCall */ private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type { - $methodReflection = $this->getMethodReflection($typeWithMethod, $methodName); - if ($methodReflection === null) { + $typeWithMethod = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($typeWithMethod === null) { return null; } + $methodReflection = $typeWithMethod->getMethod($methodName, $this); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $methodCall->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + if ($methodCall instanceof MethodCall) { + $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); + } else { + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + } + if ($normalizedMethodCall === null) { + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); + } + $resolvedTypes = []; - foreach (TypeUtils::getDirectClassNames($typeWithMethod) as $className) { - if ($methodCall instanceof MethodCall) { + foreach ($typeWithMethod->getObjectClassNames() as $className) { + if ($normalizedMethodCall instanceof MethodCall) { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) { if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) { continue; } - $resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $normalizedMethodCall, $this); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } else { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) { @@ -5125,38 +6240,35 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $methodReflection, + $normalizedMethodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } } if (count($resolvedTypes) > 0) { - return TypeCombinator::union(...$resolvedTypes); + return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall); } - return ParametersAcceptorSelector::selectFromArgs( - $this, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); } - /** @api */ - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection + /** + * @api + * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead + */ + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection { if ($typeWithProperty instanceof UnionType) { - $newTypes = []; - foreach ($typeWithProperty->getTypes() as $innerType) { - if (!$innerType->hasProperty($propertyName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithProperty = TypeCombinator::union(...$newTypes); + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes()); } if (!$typeWithProperty->hasProperty($propertyName)->yes()) { return null; @@ -5165,15 +6277,43 @@ public function getPropertyReflection(Type $typeWithProperty, string $propertyNa return $typeWithProperty->getProperty($propertyName, $this); } + /** @api */ + public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection + { + if ($typeWithProperty instanceof UnionType) { + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasInstanceProperty($propertyName)->yes()); + } + if (!$typeWithProperty->hasInstanceProperty($propertyName)->yes()) { + return null; + } + + return $typeWithProperty->getInstanceProperty($propertyName, $this); + } + + /** @api */ + public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection + { + if ($typeWithProperty instanceof UnionType) { + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasStaticProperty($propertyName)->yes()); + } + if (!$typeWithProperty->hasStaticProperty($propertyName)->yes()) { + return null; + } + + return $typeWithProperty->getStaticProperty($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 { - $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyFetch instanceof PropertyFetch) { + $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName); + } else { + $propertyReflection = $this->getStaticPropertyReflection($fetchedOnType, $propertyName); + } + if ($propertyReflection === null) { return null; } @@ -5185,142 +6325,119 @@ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Ex return $propertyReflection->getReadableType(); } - /** - * @param ConstantIntegerType|IntegerRangeType $range - * @param \PhpParser\Node\Expr\AssignOp\Div|\PhpParser\Node\Expr\AssignOp\Minus|\PhpParser\Node\Expr\AssignOp\Mul|\PhpParser\Node\Expr\AssignOp\Plus|\PhpParser\Node\Expr\BinaryOp\Div|\PhpParser\Node\Expr\BinaryOp\Minus|\PhpParser\Node\Expr\BinaryOp\Mul|\PhpParser\Node\Expr\BinaryOp\Plus $node - * @param IntegerRangeType|ConstantIntegerType|UnionType $operand - */ - private function integerRangeMath(Type $range, Expr $node, Type $operand): Type + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection { - if ($range instanceof IntegerRangeType) { - $rangeMin = $range->getMin(); - $rangeMax = $range->getMax(); - } else { - $rangeMin = $range->getValue(); - $rangeMax = $rangeMin; + if ($typeWithConstant instanceof UnionType) { + $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes()); + } + if (!$typeWithConstant->hasConstant($constantName)->yes()) { + return null; } - if ($operand instanceof UnionType) { - - $unionParts = []; + return $typeWithConstant->getConstant($constantName); + } - foreach ($operand->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($range, $node, $type); - } else { - $unionParts[] = $type->toNumber(); - } - } + public function getConstantExplicitTypeFromConfig(string $constantName, Type $constantType): Type + { + return $this->constantResolver->resolveConstantType($constantName, $constantType); + } - $union = TypeCombinator::union(...$unionParts); - if ($operand instanceof BenevolentUnionType) { - return TypeUtils::toBenevolentUnion($union)->toNumber(); + /** + * @return array + */ + private function getConstantTypes(): array + { + $constantTypes = []; + foreach ($this->expressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; } + $constantTypes[$exprString] = $typeHolder; + } + return $constantTypes; + } - return $union->toNumber(); + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); } - if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) { - if ($operand instanceof ConstantIntegerType) { - $min = $rangeMin !== null ? $rangeMin + $operand->getValue() : null; - $max = $rangeMax !== null ? $rangeMax + $operand->getValue() : null; - } else { - $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin + $operand->getMin() : null; - $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax + $operand->getMax() : null; + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); + + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); } - } elseif ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) { - if ($operand instanceof ConstantIntegerType) { - $min = $rangeMin !== null ? $rangeMin - $operand->getValue() : null; - $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) { - $min = $rangeMin - $operand->getMin(); - } else { - $min = null; - } + } - if ($operand->getMax() === null) { - $min = null; - $max = null; - } elseif ($rangeMax !== null) { - $max = $rangeMax - $operand->getMax(); - } else { - $max = null; - } + return null; + } - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; - } - } - } - } elseif ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) { - if ($operand instanceof ConstantIntegerType) { - $min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null; - $max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null; - } else { - $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null; - $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null; + /** + * @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; + } - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; + public function getIterableKeyType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } + } - // invert maximas on multiplication with negative constants - if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) - || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) - && ($min === null || $max === null)) { - [$min, $max] = [$max, $min]; - } + return $iteratee->getIterableKeyType(); + } - } else { - if ($operand instanceof ConstantIntegerType) { - $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; - $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; - } else { - $min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null; - $max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null; + public function getIterableValueType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } + } - if ($operand instanceof IntegerRangeType - && ($operand->getMin() === null || $operand->getMax() === null) - || ($rangeMin === null || $rangeMax === null) - || is_float($min) || is_float($max) - ) { - if (is_float($min)) { - $min = (int) $min; - } - if (is_float($max)) { - $max = (int) $max; - } - - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; - } + return $iteratee->getIterableValueType(); + } - // 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]; - } + public function getPhpVersion(): PhpVersions + { + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); - if ($min === null && $max === null) { - return new BenevolentUnionType([new IntegerType(), new FloatType()]); - } + $isOverallPhpVersionRange = false; + if ( + $constType instanceof IntegerRangeType + && $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID + && ($constType->getMax() === null || $constType->getMax() === PhpVersionFactory::MAX_PHP_VERSION) + ) { + $isOverallPhpVersionRange = true; + } - return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); - } + if ($constType !== null && !$isOverallPhpVersionRange) { + return new PhpVersions($constType); } - return IntegerRangeType::fromInterval($min, $max); + 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 146f6954c5..2ce18d91be 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -5,43 +5,35 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; - -/** @api */ -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; - - /** @var array alias(string) => fullName(string) */ - private array $uses; - - private ?string $className; - - private ?string $functionName; - private TemplateTypeMap $templateTypeMap; - /** @var array */ - private array $typeAliasesMap; - - private bool $bypassTypeAliases; - /** * @api - * @param string|null $namespace + * @param non-empty-string|null $namespace * @param array $uses alias(string) => fullName(string) - * @param string|null $className + * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap */ - public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, array $typeAliasesMap = [], bool $bypassTypeAliases = false) + public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = [], private ?string $typeAliasClassName = null) { - $this->namespace = $namespace; - $this->uses = $uses; - $this->className = $className; - $this->functionName = $functionName; $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); - $this->typeAliasesMap = $typeAliasesMap; - $this->bypassTypeAliases = $bypassTypeAliases; } public function getNamespace(): ?string @@ -62,14 +54,27 @@ public function hasUseAlias(string $name): bool return isset($this->uses[strtolower($name)]); } + /** + * @return array + */ + public function getConstUses(): array + { + return $this->constUses; + } + public function getClassName(): ?string { return $this->className; } + public function getClassNameForTypeAlias(): ?string + { + return $this->typeAliasClassName ?? $this->className; + } + public function resolveStringName(string $name): string { - if (strpos($name, '\\') === 0) { + if (str_starts_with($name, '\\')) { return ltrim($name, '\\'); } @@ -90,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) { @@ -119,7 +155,7 @@ public function resolveTemplateTypeName(string $name): ?Type public function withTemplateTypeMap(TemplateTypeMap $map): self { - if ($map->isEmpty()) { + if ($map->isEmpty() && $this->templateTypeMap->isEmpty()) { return $this; } @@ -130,9 +166,39 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $this->functionName, new TemplateTypeMap(array_merge( $this->templateTypeMap->getTypes(), - $map->getTypes() + $map->getTypes(), )), - $this->typeAliasesMap + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + + public function withoutNamespaceAndUses(): self + { + return new self( + null, + [], + $this->className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + + public function withClassName(string $className): self + { + return new self( + $this->namespace, + $this->uses, + $className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } @@ -149,13 +215,15 @@ public function unsetTemplateType(string $name): self $this->className, $this->functionName, $this->templateTypeMap->unsetType($name), - $this->typeAliasesMap + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } public function bypassTypeAliases(): self { - return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true); + return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true, $this->constUses); } public function shouldBypassTypeAliases(): bool @@ -168,20 +236,4 @@ public function hasTypeAlias(string $alias): bool return array_key_exists($alias, $this->typeAliasesMap); } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['namespace'], - $properties['uses'], - $properties['className'], - $properties['functionName'], - $properties['templateTypeMap'], - $properties['typeAliasesMap'] - ); - } - } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index cbde3f80ee..79e6594b46 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2,12 +2,19 @@ namespace PHPStan\Analyser; +use ArrayAccess; +use Closure; +use DivisionByZeroError; +use Override; 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; @@ -15,6 +22,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; @@ -29,6 +37,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_; @@ -38,20 +47,29 @@ 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\Reflector\ClassReflector; +use PHPStan\BetterReflection\Reflection\ReflectionEnum; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; @@ -66,154 +84,224 @@ 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\NativeTypeExpr; +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\StubPhpDocProvider; +use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\ClosureType; 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 PHPStan\Type\VoidType; - -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_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; + +#[AutowiredService] +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 StubPhpDocProvider $stubPhpDocProvider; - - private PhpVersion $phpVersion; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\File\FileHelper $fileHelper; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider; - - private bool $polluteScopeWithLoopInitialAssignments; - - private bool $polluteScopeWithAlwaysIterableForeach; - - /** @var string[][] className(string) => methods(string[]) */ - private array $earlyTerminatingMethodCalls; + /** @var array filePath(string) => bool(true) */ + private array $analysedFiles = []; - /** @var array */ - private array $earlyTerminatingFunctionCalls; + /** @var array */ + private array $earlyTerminatingMethodNames; - private bool $implicitThrows; + /** @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 $polluteScopeWithAlwaysIterableForeach * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls - * @param bool $implicitThrows + * @param string[] $universalObjectCratesClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflector $classReflector, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - Parser $parser, - FileTypeMapper $fileTypeMapper, - StubPhpDocProvider $stubPhpDocProvider, - PhpVersion $phpVersion, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - FileHelper $fileHelper, - TypeSpecifier $typeSpecifier, - DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, - bool $polluteScopeWithLoopInitialAssignments, - bool $polluteScopeWithAlwaysIterableForeach, - array $earlyTerminatingMethodCalls, - array $earlyTerminatingFunctionCalls, - bool $implicitThrows + private readonly ReflectionProvider $reflectionProvider, + private readonly InitializerExprTypeResolver $initializerExprTypeResolver, + #[AutowiredParameter(ref: '@nodeScopeResolverReflector')] + private readonly Reflector $reflector, + private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + 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 ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider, + private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, + private readonly ScopeFactory $scopeFactory, + #[AutowiredParameter] + private readonly bool $polluteScopeWithLoopInitialAssignments, + #[AutowiredParameter] + private readonly bool $polluteScopeWithAlwaysIterableForeach, + #[AutowiredParameter] + private readonly bool $polluteScopeWithBlock, + #[AutowiredParameter] + private readonly array $earlyTerminatingMethodCalls, + #[AutowiredParameter] + private readonly array $earlyTerminatingFunctionCalls, + #[AutowiredParameter] + private readonly array $universalObjectCratesClasses, + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private readonly bool $implicitThrows, + #[AutowiredParameter] + private readonly bool $treatPhpDocTypesAsCertain, + private readonly bool $narrowMethodScopeFromConstructor = true, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classReflector = $classReflector; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->phpVersion = $phpVersion; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->fileHelper = $fileHelper; - $this->typeSpecifier = $typeSpecifier; - $this->dynamicThrowTypeExtensionProvider = $dynamicThrowTypeExtensionProvider; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; - $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; - $this->earlyTerminatingMethodCalls = $earlyTerminatingMethodCalls; - $this->earlyTerminatingFunctionCalls = $earlyTerminatingFunctionCalls; - $this->implicitThrows = $implicitThrows; + $earlyTerminatingMethodNames = []; + foreach ($this->earlyTerminatingMethodCalls as $methodNames) { + foreach ($methodNames as $methodName) { + $earlyTerminatingMethodNames[strtolower($methodName)] = true; + } + } + $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; } /** @@ -227,111 +315,160 @@ public function setAnalysedFiles(array $files): void /** * @api - * @param \PhpParser\Node[] $nodes - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param Node[] $nodes + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processNodes( array $nodes, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, ): void { - $nodesCount = count($nodes); + $alreadyTerminated = false; foreach ($nodes as $i => $node) { - if (!$node instanceof Node\Stmt) { + if ( + !$node instanceof Node\Stmt + || ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) + ) { continue; } - $statementResult = $this->processStmtNode($node, $scope, $nodeCallback); + $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel()); $scope = $statementResult->getScope(); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } - if ($i < $nodesCount - 1) { - $nextStmt = $nodes[$i + 1]; - if (!$nextStmt instanceof Node\Stmt) { - continue; - } + $alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $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 callable(\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, - callable $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(), - $statementResult->getThrowPoints() - ), - $parentNode->returnType !== null - ), $scope); + $endStatements = $statementResult->getEndStatements(); + if (count($endStatements) > 0) { + foreach ($endStatements as $endStatement) { + $endStatementResult = $endStatement->getResult(); + $nodeCallback(new ExecutionEndNode( + $endStatement->getStatement(), + new StatementResult( + $endStatementResult->getScope(), + $hasYield, + $endStatementResult->isAlwaysTerminating(), + $endStatementResult->getExitPoints(), + $endStatementResult->getThrowPoints(), + $endStatementResult->getImpurePoints(), + ), + $parentNode->getReturnType() !== null, + ), $endStatementResult->getScope()); + } + } else { + $nodeCallback(new ExecutionEndNode( + $stmt, + new StatementResult( + $scope, + $hasYield, + $statementResult->isAlwaysTerminating(), + $statementResult->getExitPoints(), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + ), + $parentNode->getReturnType() !== null, + ), $scope); + } } $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints()); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } $alreadyTerminated = true; - if ($i < $stmtCount - 1) { - $nextStmt = $stmts[$i + 1]; - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); - } - break; + $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); } - $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints); + $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); if ($stmtCount === 0 && $shouldCheckLastStatement) { - /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ - $parentNode = $parentNode; + $returnTypeNode = $parentNode->getReturnType(); + if ($parentNode instanceof Expr\Closure) { + $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + } $nodeCallback(new ExecutionEndNode( $parentNode, $statementResult, - $parentNode->returnType !== null + $returnTypeNode !== null, ), $scope); } @@ -339,33 +476,29 @@ public function processStmtNodes( } /** - * @param \PhpParser\Node\Stmt $stmt - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, + StatementContext $context, ): StatementResult { if ( - $stmt instanceof Throw_ - || $stmt instanceof Return_ - ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr); - } elseif ( !$stmt instanceof Static_ && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ + && !$stmt instanceof Node\Stmt\Property + && !$stmt instanceof Node\Stmt\ClassConst + && !$stmt instanceof Node\Stmt\Const_ ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } if ($stmt instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ( $scope->isInTrait() @@ -373,30 +506,41 @@ private function processStmtNode( ) { $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); if ($methodReflection instanceof NativeMethodReflection) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } if ($methodReflection instanceof PhpMethodReflection) { $declaringTrait = $methodReflection->getDeclaringTrait(); if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } } } } - $nodeCallback($stmt, $scope); + $stmtScope = $scope; + if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) { + $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr->expr, $nodeCallback); + } + if ($stmt instanceof Return_) { + $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback); + } + + $nodeCallback($stmt, $stmtScope); $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); if ($stmt instanceof Node\Stmt\Declare_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $alwaysTerminating = false; + $exitPoints = []; foreach ($stmt->declares as $declare) { $nodeCallback($declare, $scope); $nodeCallback($declare->value, $scope); if ( $declare->key->name !== 'strict_types' - || !($declare->value instanceof Node\Scalar\LNumber) + || !($declare->value instanceof Node\Scalar\Int_) || $declare->value->value !== 1 ) { continue; @@ -404,26 +548,37 @@ private function processStmtNode( $scope = $scope->enterDeclareStrictTypes(); } + + if ($stmt->stmts !== null) { + $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $alwaysTerminating = $result->isAlwaysTerminating(); + $exitPoints = $result->getExitPoints(); + } + + return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Function_) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { $nodeCallback($stmt->returnType, $scope); } + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); + } + $functionScope = $scope->enterFunction( $stmt, $templateTypeMap, @@ -433,14 +588,26 @@ private function processStmtNode( $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, - $isPure + $isPure, + $acceptsNamedArguments, + $asserts, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); - $nodeCallback(new InFunctionNode($stmt), $functionScope); + $functionReflection = $functionScope->getFunction(); + if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + + $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope); $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$executionEnds): void { + $functionImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -448,43 +615,61 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + $functionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); $nodeCallback(new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, - $executionEnds + $executionEnds, + array_merge($statementResult->getImpurePoints(), $functionImpurePoints), + $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { $nodeCallback($stmt->returnType, $scope); } + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); + } + + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct'; + $methodScope = $scope->enterClassMethod( $stmt, $templateTypeMap, @@ -495,17 +680,31 @@ private function processStmtNode( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $isConstructor, ); - if ($stmt->name->toLowerString() === '__construct') { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + + if ($isConstructor) { foreach ($stmt->params as $param) { - if ($param->flags === 0) { + if ($param->flags === 0 && $param->hooks === []) { continue; } - if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') { + throw new ShouldNotHappenException(); } $phpDoc = null; if ($param->getDocComment() !== null) { @@ -514,23 +713,46 @@ private function processStmtNode( $nodeCallback(new ClassPropertyNode( $param->var->name, $param->flags, - $param->type, - $param->default, + $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null, + null, $phpDoc, + $phpDocParameterTypes[$param->var->name] ?? null, true, - $param + $isFromTrait, + $param, + $isReadOnly, + $scope->isInTrait(), + $classReflection->isReadOnly(), + false, + $classReflection, ), $methodScope); + $this->processPropertyHooks( + $stmt, + $param->type, + $phpDocParameterTypes[$param->var->name] ?? null, + $param->var->name, + $param->hooks, + $scope, + $nodeCallback, + ); + $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType()); } } if ($stmt->getAttribute('virtual', false) === false) { - $nodeCallback(new InClassMethodNode($stmt), $methodScope); + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope); } if ($stmt->stmts !== null) { $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$executionEnds): void { + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -538,101 +760,212 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + if ( + $node->getPropertyFetch() instanceof Expr\PropertyFetch + && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection + && $scope->getFunction()->getDeclaringClass()->hasConstructor() + && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() + && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + ) { + return; + } + $methodImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); + + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new MethodReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, - $executionEnds + $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 = []; + $isAlwaysTerminating = false; 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(); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } $throwPoints = $overridingThrowPoints ?? $throwPoints; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'echo', 'echo', true), + ]; + return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints); } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); $hasYield = $result->hasYield(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) { if ($stmt->num !== null) { - $result = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Expression) { + if ($stmt->expr instanceof Expr\Throw_) { + $scope = $stmtScope; + } $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasAssign = false; + $currentScope = $scope; + $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $nodeCallback($node, $scope); + if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) { + return; + } + if ($scope->getFunction() !== $currentScope->getFunction()) { + return; + } + if (!$node instanceof VariableAssignNode && !$node instanceof PropertyAssignNode) { + return; + } + + $hasAssign = true; + }, ExpressionContext::createTopLevel()); + $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); + if ( + count($result->getImpurePoints()) === 0 + && count($throwPoints) === 0 + && !$stmt->expr instanceof Expr\PostInc + && !$stmt->expr instanceof Expr\PreInc + && !$stmt->expr instanceof Expr\PostDec + && !$stmt->expr instanceof Expr\PreDec + ) { + $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope); + } $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, $stmt->expr, - TypeSpecifierContext::createNull() + TypeSpecifierContext::createNull(), )); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints); + return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Namespace_) { if ($stmt->name !== null) { $scope = $scope->enterNamespace($stmt->name->toString()); } - $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback)->getScope(); + $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope(); $hasYield = false; $throwPoints = []; + $impurePoints = []; } elseif ($stmt instanceof Node\Stmt\Trait_) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { + if (!$context->isTopLevel()) { + return new StatementResult($scope, false, false, [], [], []); + } $hasYield = false; $throwPoints = []; + $impurePoints = []; if (isset($stmt->namespacedName)) { - $classReflection = $this->getCurrentClassReflection($stmt, $scope); + $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ($stmt->getAttribute('anonymousClass', false) === false) { + if (!$stmt->isAnonymous()) { $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); } else { $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); @@ -640,83 +973,137 @@ private function processStmtNode( $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $classScope, $nodeCallback, ExpressionContext::createDeep()); + $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer); + + $classLikeStatements = $stmt->stmts; + if ($this->narrowMethodScopeFromConstructor) { + // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope + usort($classLikeStatements, static function ($a, $b) { + if ($a instanceof Node\Stmt\Property) { + return 1; } - } + if ($b instanceof Node\Stmt\Property) { + return -1; + } + + if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) { + return 0; + } + + return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct']; + }); } - $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); - $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer); - $nodeCallback(new ClassPropertiesNode($stmt, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope); + $this->processStmtNodes($stmt, $classLikeStatements, $classScope, $classStatementsGatherer, $context); + $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope); + $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope); + $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope); + $classReflection->evictPrivateSymbols(); + $this->calledMethodResults = []; } elseif ($stmt instanceof Node\Stmt\Property) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + + $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null; + + [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); + $phpDocType = null; + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); } + foreach ($stmt->props as $prop) { - $this->processStmtNode($prop, $scope, $nodeCallback); - $docComment = $stmt->getDocComment(); + $nodeCallback($prop, $scope); + if ($prop->default !== null) { + $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $propertyName = $prop->name->toString(); + + if ($phpDocType === null) { + if (isset($varTags[$propertyName])) { + $phpDocType = $varTags[$propertyName]->getType(); + } + } + + $propStmt = clone $stmt; + $propStmt->setAttributes($prop->getAttributes()); $nodeCallback( new ClassPropertyNode( - $prop->name->toString(), + $propertyName, $stmt->flags, - $stmt->type, + $nativePropertyType, $prop->default, - $docComment !== null ? $docComment->getText() : null, + $docComment, + $phpDocType, false, - $prop + false, + $propStmt, + $isReadOnly, + $scope->isInTrait(), + $scope->getClassReflection()->isReadOnly(), + $isAllowedPrivateMutation, + $scope->getClassReflection(), ), - $scope + $scope, + ); + } + + if (count($stmt->hooks) > 0) { + if (!isset($propertyName)) { + throw new ShouldNotHappenException('Property name should be known when analysing hooks.'); + } + $this->processPropertyHooks( + $stmt, + $stmt->type, + $phpDocType, + $propertyName, + $stmt->hooks, + $scope, + $nodeCallback, ); } if ($stmt->type !== null) { $nodeCallback($stmt->type, $scope); } - } elseif ($stmt instanceof Node\Stmt\PropertyProperty) { - $hasYield = false; - $throwPoints = []; - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } elseif ($stmt instanceof Throw_) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); - $throwPoints = $result->getThrowPoints(); - $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false); - return new StatementResult($result->getScope(), $result->hasYield(), true, [ - new StatementExitPoint($stmt, $scope), - ], $throwPoints); } elseif ($stmt instanceof If_) { - $conditionType = $scope->getType($stmt->cond)->toBoolean(); - $ifAlwaysTrue = $conditionType instanceof ConstantBooleanType && $conditionType->getValue(); - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $ifAlwaysTrue = $conditionType->isTrue()->yes(); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $endStatements = []; $finalScope = null; $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); - if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { + if (!$conditionType->isTrue()->no()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult); + } $hasYield = $branchScopeStatementResult->hasYield() || $hasYield; } @@ -726,33 +1113,36 @@ 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 - && ( - !$lastElseIfConditionIsTrue - && ( - !$elseIfConditionType instanceof ConstantBooleanType - || $elseIfConditionType->getValue() - ) - ) + && !$lastElseIfConditionIsTrue + && !$elseIfConditionType->isTrue()->no() ) { $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; } @@ -762,20 +1152,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(); } } @@ -784,45 +1182,60 @@ private function processStmtNode( $finalScope = $scope; } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); + if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + $endStatements[] = new EndStatementResult($stmt, new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints)); + } + + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements); } elseif ($stmt instanceof Node\Stmt\TraitUse) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processTraitUse($stmt, $scope, $nodeCallback); } elseif ($stmt instanceof Foreach_) { - $condResult = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, - new Array_([]) + new Array_([]), ); - $bodyScope = $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - if ($bodyScope->equals($prevScope)) { - break; - } + 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, $nodeCallback); + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback); + $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); @@ -831,15 +1244,20 @@ private function processStmtNode( $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $isIterableAtLeastOnce = $scope->getType($stmt->expr)->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + $exprType = $scope->getType($stmt->expr); + $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); + if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { + $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( + new BinaryOp\Identical( + $stmt->expr, + new Array_([]), + ), + new FuncCall(new Name\FullyQualified('is_object'), [ + new Arg($stmt->expr), + ]), + ))); + } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; - } elseif ($isIterableAtLeastOnce->maybe()) { - if ($this->polluteScopeWithAlwaysIterableForeach) { - $finalScope = $finalScope->mergeWith($scope->filterByFalseyValue($arrayComparisonExpr)); - } else { - $finalScope = $finalScope->mergeWith($scope); - } } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables @@ -847,6 +1265,10 @@ private function processStmtNode( if (!$isIterableAtLeastOnce->no()) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); + } + if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { + $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr); } return new StatementResult( @@ -854,53 +1276,75 @@ private function processStmtNode( $finalScopeResult->hasYield() || $condResult->hasYield(), $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(), $finalScopeResult->getExitPointsForOuterLoop(), - $throwPoints + $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; + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $condScope = $condResult->getFalseyScope(); + if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { + $scope = $condScope->mergeWith($scope); } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + return new StatementResult( + $scope, + $condResult->hasYield(), + false, + [], + $condResult->getThrowPoints(), + $condResult->getImpurePoints(), + ); + } + $bodyScope = $condResult->getTruthyScope(); + + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); + $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel(); + if (!$alwaysIterates) { + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + } } + $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); foreach ($breakExitPoints as $breakExitPoint) { $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); } - $beforeCondBooleanType = $scope->getType($stmt->cond)->toBoolean(); - $condBooleanType = $bodyScopeMaybeRan->getType($stmt->cond)->toBoolean(); - $isIterableAtLeastOnce = $beforeCondBooleanType instanceof ConstantBooleanType && $beforeCondBooleanType->getValue(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); - $neverIterates = $condBooleanType instanceof ConstantBooleanType && !$condBooleanType->getValue(); + $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan); if ($alwaysIterates) { @@ -910,7 +1354,6 @@ private function processStmtNode( } else { $isAlwaysTerminating = false; } - $condScope = $condResult->getFalseyScope(); if (!$isIterableAtLeastOnce) { if (!$this->polluteScopeWithLoopInitialAssignments) { $condScope = $condScope->mergeWith($scope); @@ -919,8 +1362,10 @@ private function processStmtNode( } $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); if (!$neverIterates) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } return new StatementResult( @@ -928,7 +1373,8 @@ private function processStmtNode( $finalScopeResult->hasYield() || $condResult->hasYield(), $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), - $throwPoints + $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; @@ -936,41 +1382,45 @@ private function processStmtNode( $count = 0; $hasYield = false; $throwPoints = []; + $impurePoints = []; + + if ($context->isTopLevel()) { + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + if ($bodyScope->equals($prevScope)) { + break; + } - do { - $prevScope = $bodyScope; - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); - foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); - } - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - if ($bodyScope->equals($prevScope)) { - break; - } - - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $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(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); } - $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope); @@ -984,12 +1434,13 @@ private function processStmtNode( $finalScope = $scope; } if (!$alwaysTerminating) { - $condResult = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $finalScope = $condResult->getFalseyScope(); } else { - $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); @@ -1000,93 +1451,142 @@ private function processStmtNode( $bodyScopeResult->hasYield() || $hasYield, $alwaysTerminating, $bodyScopeResult->getExitPointsForOuterLoop(), - array_merge($throwPoints, $bodyScopeResult->getThrowPoints()) + array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), + array_merge($impurePoints, $bodyScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof For_) { $initScope = $scope; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->init as $initExpr) { - $initResult = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); + $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); $initScope = $initResult->getScope(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); } $bodyScope = $initScope; + $isIterableAtLeastOnce = TrinaryLogic::createYes(); + $lastCondExpr = $stmt->cond[count($stmt->cond) - 1] ?? null; foreach ($stmt->cond as $condExpr) { - $condResult = $this->processExprNode($condExpr, $bodyScope, static function (): void { + $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); + $initScope = $condResult->getScope(); + $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) { - $exprResult = $this->processExprNode($loopExpr, $bodyScope, static function (): void { - }, ExpressionContext::createTopLevel()); - $bodyScope = $exprResult->getScope(); - $hasYield = $hasYield || $exprResult->hasYield(); - $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); - } + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($initScope); + if ($lastCondExpr !== null) { + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + } + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + foreach ($stmt->loop as $loopExpr) { + $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void { + }, ExpressionContext::createTopLevel()); + $bodyScope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + } - if ($bodyScope->equals($prevScope)) { - break; - } + if ($bodyScope->equals($prevScope)) { + break; + } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $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() || $hasYield, - false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, + $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), - array_merge($throwPoints, $finalScopeResult->getThrowPoints()) + array_merge($throwPoints, $finalScopeResult->getThrowPoints()), + array_merge($impurePoints, $finalScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof Switch_) { - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; @@ -1096,21 +1596,26 @@ private function processStmtNode( $hasYield = $condResult->hasYield(); $exitPointsForOuterLoop = []; $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $fullCondExpr = null; foreach ($stmt->cases as $caseNode) { if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); - $caseResult = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); + $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr); + $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); $scopeForBranches = $caseResult->getScope(); $hasYield = $hasYield || $caseResult->hasYield(); $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); - $branchScope = $scopeForBranches->filterByTruthyValue($condExpr); + $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints()); + $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; + $fullCondExpr = null; $branchScope = $scopeForBranches; } $branchScope = $branchScope->mergeWith($prevScope); - $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints(); $hasYield = $hasYield || $branchFinalScopeResult->hasYield(); @@ -1123,11 +1628,13 @@ private function processStmtNode( } $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop()); $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints()); if ($branchScopeResult->isAlwaysTerminating()) { $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); $prevScope = null; - if (isset($condExpr)) { - $scopeForBranches = $scopeForBranches->filterByFalseyValue($condExpr); + if (isset($fullCondExpr)) { + $scopeForBranches = $scopeForBranches->filterByFalseyValue($fullCondExpr); + $fullCondExpr = null; } if (!$branchFinalScopeResult->isAlwaysTerminating()) { $finalScope = $branchScope->mergeWith($finalScope); @@ -1137,7 +1644,9 @@ private function processStmtNode( } } - if (!$hasDefaultCase) { + $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType; + + if (!$hasDefaultCase && !$exhaustive) { $alwaysTerminating = false; } @@ -1146,13 +1655,13 @@ private function processStmtNode( $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); } - if (!$hasDefaultCase || $finalScope === null) { + if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) { $finalScope = $scope->mergeWith($finalScope); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints); } elseif ($stmt instanceof TryCatch) { - $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; @@ -1168,7 +1677,7 @@ private function processStmtNode( } foreach ($branchScopeResult->getExitPoints() as $exitPoint) { $finallyExitPoints[] = $exitPoint; - if ($exitPoint->getStatement() instanceof Throw_) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { continue; } if ($finallyScope !== null) { @@ -1178,62 +1687,115 @@ private function processStmtNode( } $throwPoints = $branchScopeResult->getThrowPoints(); + $impurePoints = $branchScopeResult->getImpurePoints(); $throwPointsForLater = []; $pastCatchTypes = new NeverType(); foreach ($stmt->catches as $catchNode) { $nodeCallback($catchNode, $scope); - $catchType = TypeCombinator::union(...array_map(static function (Name $name): Type { - return new ObjectType($name->toString()); - }, $catchNode->types)); - $originalCatchType = $catchType; - $catchType = TypeCombinator::remove($catchType, $pastCatchTypes); + $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types); + $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes); + + $originalCatchType = TypeCombinator::union(...$originalCatchTypes); + $catchType = TypeCombinator::union(...$catchTypes); $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType); + $matchingThrowPoints = []; - $newThrowPoints = []; - foreach ($throwPoints as $throwPoint) { - if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false); + + // throwable matches all + foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) { + if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { continue; } - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if ($isSuperType->no()) { - continue; + + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + $matchingCatchTypes[$catchTypeIndex] = true; } - $matchingThrowPoints[] = $throwPoint; } - $hasExplicit = count($matchingThrowPoints) > 0; - foreach ($throwPoints as $throwPoint) { - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if (!$hasExplicit && !$isSuperType->no()) { - $matchingThrowPoints[] = $throwPoint; + + // explicit only + $onlyExplicitIsThrow = true; + if (count($matchingThrowPoints) === 0) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingCatchTypes[$catchTypeIndex] = true; + if (!$throwPoint->isExplicit()) { + continue; + } + $throwNode = $throwPoint->getNode(); + if ( + !$throwNode instanceof Expr\Throw_ + && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_) + ) { + $onlyExplicitIsThrow = false; + } + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - if ($isSuperType->yes()) { - continue; + } + + // implicit only + if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + if ($throwPoint->isExplicit()) { + continue; + } + + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - $newThrowPoints[] = $throwPoint->subtractCatchType($catchType); } - $throwPoints = $newThrowPoints; + // include previously removed throw points if (count($matchingThrowPoints) === 0) { - $throwableThrowPoints = []; - if ($originalCatchType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { + if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { if (!$originalThrowPoint->canContainAnyThrowable()) { continue; } - $throwableThrowPoints[] = $originalThrowPoint; + $matchingThrowPoints[] = $originalThrowPoint; + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true); } } + } + + // emit error + foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { + if ($matched) { + continue; + } + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); + } + + if (count($matchingThrowPoints) === 0) { + continue; + } + + // recompute throw points + $newThrowPoints = []; + foreach ($throwPoints as $throwPoint) { + $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType); - if (count($throwableThrowPoints) === 0) { - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope); + if ($newThrowPoint->getType() instanceof NeverType) { continue; } - $matchingThrowPoints = $throwableThrowPoints; + $newThrowPoints[] = $newThrowPoint; } + $throwPoints = $newThrowPoints; $catchScope = null; foreach ($matchingThrowPoints as $matchingThrowPoint) { @@ -1247,19 +1809,20 @@ private function processStmtNode( $variableName = null; if ($catchNode->var !== null) { if (!is_string($catchNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableName = $catchNode->var->name; } - $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback); + $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context); $catchScopeForFinally = $catchScopeResult->getScope(); $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); $hasYield = $hasYield || $catchScopeResult->hasYield(); $catchThrowPoints = $catchScopeResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints()); $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints); if ($finallyScope !== null) { @@ -1267,7 +1830,7 @@ private function processStmtNode( } foreach ($catchScopeResult->getExitPoints() as $exitPoint) { $finallyExitPoints[] = $exitPoint; - if ($exitPoint->getStatement() instanceof Throw_) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { continue; } if ($finallyScope !== null) { @@ -1295,124 +1858,282 @@ private function processStmtNode( $finallyScope = $finallyScope->mergeWith($throwPoint->getScope()); } - if ($finallyScope !== null && $stmt->finally !== null) { + if ($finallyScope !== null) { $originalFinallyScope = $finallyScope; - $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback); + $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints()); $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); if (count($finallyResult->getExitPoints()) > 0) { $nodeCallback(new FinallyExitPointsNode( $finallyResult->getExitPoints(), - $finallyExitPoints + $finallyExitPoints, ), $scope); } $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater)); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints); } elseif ($stmt instanceof Unset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->vars as $var) { - $scope = $this->lookForEnterVariableAssign($scope, $var); - $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); - $scope = $this->lookForExitVariableAssign($scope, $var); - $scope = $scope->unsetExpression($var); + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); + $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $exprResult->getScope(); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + if ($var instanceof ArrayDimFetch && $var->dim !== null) { + $cloningTraverser = new NodeTraverser(); + $cloningTraverser->addVisitor(new CloningVisitor()); + + /** @var Expr $clonedVar */ + [$clonedVar] = $cloningTraverser->traverse([$var->var]); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class () extends NodeVisitorAbstract { + + #[Override] + 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->processVirtualAssign($scope, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->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; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'static', + 'static variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { - $scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope(); if (!is_string($var->var->name)) { - continue; + throw new ShouldNotHappenException(); + } + + if ($var->default !== null) { + $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } + $scope = $scope->enterExpressionAssign($var->var); + $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); + $scope = $scope->exitExpressionAssign($var->var); + + $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); - } elseif ($stmt instanceof StaticVar) { + } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; $throwPoints = []; - if (!is_string($stmt->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = []; + foreach ($stmt->consts as $const) { + $nodeCallback($const, $scope); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($const->namespacedName !== null) { + $constantName = new Name\FullyQualified($const->namespacedName->toString()); + } else { + $constantName = new Name\FullyQualified($const->name->toString()); + } + $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); } - $scope = $scope->enterExpressionAssign($stmt->var); - $this->processExprNode($stmt->var, $scope, $nodeCallback, ExpressionContext::createDeep()); - $scope = $scope->exitExpressionAssign($stmt->var); - $scope = $scope->assignVariable($stmt->var->name, new MixedType()); - } elseif ($stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\ClassConst) { + } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; $throwPoints = []; - if ($stmt instanceof Node\Stmt\ClassConst) { - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - } + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - if ($scope->getNamespace() !== null) { - $constName = [$scope->getNamespace(), $const->name->toString()]; - } else { - $constName = $const->name->toString(); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($scope->getClassReflection() === null) { + throw new ShouldNotHappenException(); } - $scope = $scope->specifyExpressionType(new ConstFetch(new Name\FullyQualified($constName)), $scope->getType($const->value)); + $scope = $scope->assignExpression( + new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), + $scope->getType($const->value), + $scope->getNativeType($const->value), + ); + } + } elseif ($stmt instanceof Node\Stmt\EnumCase) { + $hasYield = false; + $throwPoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $impurePoints = []; + if ($stmt->expr !== null) { + $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = $exprResult->getImpurePoints(); + } + } elseif ($stmt instanceof InlineHTML) { + $hasYield = false; + $throwPoints = []; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true), + ]; + } elseif ($stmt instanceof Node\Stmt\Block) { + $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + if ($this->polluteScopeWithBlock) { + return $result; } + + return new StatementResult( + $scope->mergeWith($result->getScope()), + $result->hasYield(), + $result->isAlwaysTerminating(), + $result->getExitPoints(), + $result->getThrowPoints(), + $result->getImpurePoints(), + $result->getEndStatements(), + ); } elseif ($stmt instanceof Node\Stmt\Nop) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; + } elseif ($stmt instanceof Node\Stmt\GroupUse) { + $hasYield = false; + $throwPoints = []; + foreach ($stmt->uses as $use) { + $nodeCallback($use, $scope); + } + $impurePoints = []; } else { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; + } + + return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); + } + + /** + * @return array{bool, string|null} + */ + private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array + { + $initializerExprContext = InitializerExprContext::fromStubParameter( + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->getFile(), + $stmt, + ); + $isDeprecated = false; + $deprecatedDescription = null; + $deprecatedDescriptionType = null; + foreach ($stmt->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() !== 'Deprecated') { + continue; + } + $isDeprecated = true; + $arguments = $attr->args; + foreach ($arguments as $i => $arg) { + $argName = $arg->name; + if ($argName === null) { + if ($i !== 0) { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + + if ($argName->toString() !== 'message') { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + } } - return new StatementResult($scope, $hasYield, false, [], $throwPoints); + if ($deprecatedDescriptionType !== null) { + $constantStrings = $deprecatedDescriptionType->getConstantStrings(); + if (count($constantStrings) === 1) { + $deprecatedDescription = $constantStrings[0]->getValue(); + } + } + + return [$isDeprecated, $deprecatedDescription]; } /** - * @param Node\Stmt $statement - * @param MutatingScope $scope * @return ThrowPoint[]|null */ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array @@ -1428,13 +2149,13 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); $throwsTag = $resolvedPhpDoc->getThrowsTag(); if ($throwsTag !== null) { $throwsType = $throwsTag->getType(); - if ($throwsType instanceof VoidType) { + if ($throwsType->isVoid()->yes()) { return []; } @@ -1445,171 +2166,165 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s return null; } - private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, Scope $scope): ClassReflection + private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { - $className = $stmt->namespacedName->toString(); if (!$this->reflectionProvider->hasClass($className)) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } - $defaultClassReflection = $this->reflectionProvider->getClass($stmt->namespacedName->toString()); + $defaultClassReflection = $this->reflectionProvider->getClass($className); if ($defaultClassReflection->getFileName() !== $scope->getFile()) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } $startLine = $defaultClassReflection->getNativeReflection()->getStartLine(); if ($startLine !== $stmt->getStartLine()) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } return $defaultClassReflection; } - private function createAstClassReflection(Node\Stmt\ClassLike $stmt, Scope $scope): ClassReflection + private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { $nodeToReflection = new NodeToReflection(); $betterReflectionClass = $nodeToReflection->__invoke( - $this->classReflector, + $this->reflector, $stmt, - new LocatedSource(FileReader::read($scope->getFile()), $scope->getFile()), - $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null + new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()), + $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null, ); if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } + $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true); + return new ClassReflection( $this->reflectionProvider, + $this->initializerExprTypeResolver, $this->fileTypeMapper, $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, + $this->classReflectionExtensionRegistryProvider->getRegistry()->getPhpClassReflectionExtension(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), $betterReflectionClass->getName(), - new ReflectionClass($betterReflectionClass), + $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass), null, null, null, - sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()) + $this->universalObjectCratesClasses, + sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()), ); } - private function lookForEnterVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope + private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $scope->enterExpressionAssign($expr); - } - if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->enterExpressionAssign($expr); - }); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); } - private function lookForExitVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope + private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - $scope = $scope->exitExpressionAssign($expr); - if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->exitExpressionAssign($expr); - }); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); } /** - * @param MutatingScope $scope - * @param Expr $expr - * @param \Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback - * @return MutatingScope + * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback */ - private function lookForVariableAssignCallback(MutatingScope $scope, Expr $expr, \Closure $callback): MutatingScope + private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope { - if ($expr instanceof Variable) { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { $scope = $callback($scope, $expr); - } elseif ($expr instanceof ArrayDimFetch) { - while ($expr instanceof ArrayDimFetch) { - $expr = $expr->var; - } + } - $scope = $this->lookForVariableAssignCallback($scope, $expr, $callback); + if ($expr instanceof ArrayDimFetch) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { - $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); - } elseif ($expr instanceof StaticPropertyFetch) { - if ($expr->class instanceof Expr) { - $scope = $this->lookForVariableAssignCallback($scope, $expr->class, $callback); - } - } elseif ($expr instanceof Array_ || $expr instanceof List_) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { + $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); + } elseif ($expr instanceof List_) { foreach ($expr->items as $item) { if ($item === null) { continue; } - $scope = $this->lookForVariableAssignCallback($scope, $item->value, $callback); + $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); } } return $scope; } - private function ensureShallowNonNullability(MutatingScope $scope, Expr $exprToSpecify): EnsuredNonNullabilityResult + private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult { $exprType = $scope->getType($exprToSpecify); + $isNull = $exprType->isNull(); + if ($isNull->yes()) { + return new EnsuredNonNullabilityResult($scope, []); + } + + // keep certainty + $certainty = TrinaryLogic::createYes(); + $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify); + if (!$hasExpressionType->no()) { + $certainty = $hasExpressionType; + } + $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); - if (!$exprType->equals($exprTypeWithoutNull)) { - $nativeType = $scope->getNativeType($exprToSpecify); - $scope = $scope->specifyExpressionType( - $exprToSpecify, - $exprTypeWithoutNull, - TypeCombinator::removeNull($nativeType) - ); + if ($exprType->equals($exprTypeWithoutNull)) { + $originalExprType = $originalScope->getType($exprToSpecify); + if (!$originalExprType->equals($exprTypeWithoutNull)) { + $originalNativeType = $originalScope->getNativeType($exprToSpecify); - return new EnsuredNonNullabilityResult( - $scope, - [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType), - ] - ); + return new EnsuredNonNullabilityResult($scope, [ + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), + ]); + } + return new EnsuredNonNullabilityResult($scope, []); } - return new EnsuredNonNullabilityResult($scope, []); + $nativeType = $scope->getNativeType($exprToSpecify); + $scope = $scope->specifyExpressionType( + $exprToSpecify, + $exprTypeWithoutNull, + TypeCombinator::removeNull($nativeType), + TrinaryLogic::createYes(), + ); + + return new EnsuredNonNullabilityResult( + $scope, + [ + new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty), + ], + ); } - private function ensureNonNullability(MutatingScope $scope, Expr $expr, bool $findMethods): EnsuredNonNullabilityResult + private function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult { - $exprToSpecify = $expr; $specifiedExpressions = []; - while (true) { - $result = $this->ensureShallowNonNullability($scope, $exprToSpecify); - $scope = $result->getScope(); + $originalScope = $scope; + $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) { + $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr); foreach ($result->getSpecifiedExpressions() as $specifiedExpression) { $specifiedExpressions[] = $specifiedExpression; } - - if ($exprToSpecify instanceof PropertyFetch) { - $exprToSpecify = $exprToSpecify->var; - } elseif ($exprToSpecify instanceof StaticPropertyFetch && $exprToSpecify->class instanceof Expr) { - $exprToSpecify = $exprToSpecify->class; - } elseif ($findMethods && $exprToSpecify instanceof MethodCall) { - $exprToSpecify = $exprToSpecify->var; - } elseif ($findMethods && $exprToSpecify instanceof StaticCall && $exprToSpecify->class instanceof Expr) { - $exprToSpecify = $exprToSpecify->class; - } else { - break; - } - } + return $result->getScope(); + }); return new EnsuredNonNullabilityResult($scope, $specifiedExpressions); } /** - * @param MutatingScope $scope * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions - * @return MutatingScope */ private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope { @@ -1617,7 +2332,8 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr $scope = $scope->specifyExpressionType( $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), - $specifiedExpressionResult->getOriginalNativeType() + $specifiedExpressionResult->getOriginalNativeType(), + $specifiedExpressionResult->getCertainty(), ); } @@ -1627,7 +2343,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr { if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) { - if (count($this->earlyTerminatingMethodCalls) > 0) { + if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { if ($expr instanceof MethodCall) { $methodCalledOnType = $scope->getType($expr->var); } else { @@ -1638,8 +2354,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } } - $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); - foreach ($directClassNames as $referencedClass) { + foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { continue; } @@ -1664,6 +2379,10 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } } + if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) { + return $expr; + } + $exprType = $scope->getType($expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; @@ -1673,147 +2392,228 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } /** - * @param \PhpParser\Node\Expr $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param \PHPStan\Analyser\ExpressionContext $context - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processExprNode(Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult + 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 = []; + $isAlwaysTerminating = false; 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(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - - if ($expr instanceof AssignRef) { - $scope = $scope->exitExpressionAssign($expr->expr); - } - - return new ExpressionResult($scope, $hasYield, $throwPoints); - }, - true - ); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $varChangedScope = false; - if ($expr->var instanceof Variable && is_string($expr->var->name)) { - $scope = $this->processVarAnnotation($scope, [$expr->var->name], $expr, $varChangedScope); - } - - if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); - } - } else { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - foreach ($expr->var->items as $arrayItem) { - if ($arrayItem === null) { - continue; + $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); - $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $itemResult->hasYield(); - $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); - } - $scope = $this->lookForArrayDestructuringArray($scope, $expr->var, $scope->getType($expr->expr)); - $vars = $this->getAssignedVariables($expr->var); - if (count($vars) > 0) { - $varChangedScope = false; - $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope); - if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); + if ($expr instanceof AssignRef) { + $scope = $scope->exitExpressionAssign($expr->expr); } + + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + }, + true, + ); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $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->isAlwaysTerminating(), + $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(); - } elseif ($expr instanceof FuncCall) { - $parametersAcceptor = null; + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + 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 = []; + $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $nameType->getCallableParametersAcceptors($scope) + $nameType->getCallableParametersAcceptors($scope), + null, ); } - $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); - $throwPoints = $nameResult->getThrowPoints(); + + $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $scope = $nameResult->getScope(); + $throwPoints = $nameResult->getThrowPoints(); + $impurePoints = $nameResult->getImpurePoints(); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); + 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()); + $isAlwaysTerminating = $invokeResult->isAlwaysTerminating(); + } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); + + $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); + } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $functionReflection->getVariants() + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'functionCall', + 'call to unknown function', + false, ); } - $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit(); + } + $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); - if (isset($functionReflection)) { + if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; @@ -1823,7 +2623,14 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0 + && $scope->isInClass() + ) { + $scope = $scope->invalidateExpression(new Variable('this'), true); + } + + if ( + $functionReflection !== null && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) ) { $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) @@ -1833,136 +2640,188 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $functionReflection !== null + && $functionReflection->getName() === 'file_put_contents' + && count($expr->getArgs()) > 0 + ) { + $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$expr->getArgs()[0]])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$expr->getArgs()[0]])); + } + + if ( + $functionReflection !== null && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; - $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); - $scope = $scope->invalidateExpression($arrayArg); - if (count($constantArrays) > 0) { - $resultArrayTypes = []; - foreach ($constantArrays as $constantArray) { - if ($functionReflection->getName() === 'array_pop') { - $resultArrayTypes[] = $constantArray->removeLast(); - } else { - $resultArrayTypes[] = $constantArray->removeFirst(); - } - } + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); + $isArrayPop = $functionReflection->getName() === 'array_pop'; - $scope = $scope->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$resultArrayTypes) - ); - } else { - $arrays = TypeUtils::getAnyArrays($scope->getType($arrayArg)); - if (count($arrays) > 0) { - $scope = $scope->specifyExpressionType($arrayArg, TypeCombinator::union(...$arrays)); - } - } + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $arrayArg, + new NativeTypeExpr( + $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), + $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), + ), + $nodeCallback, + )->getScope(); } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) && count($expr->getArgs()) >= 2 ) { - $argumentTypes = []; - foreach (array_slice($expr->getArgs(), 1) as $callArg) { - $callArgType = $scope->getType($callArg->value); - if ($callArg->unpack) { - $iterableValueType = $callArgType->getIterableValueType(); - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - continue; - } + $arrayArg = $expr->getArgs()[0]->value; - $argumentTypes[] = $callArgType; - } + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $arrayArg, + new NativeTypeExpr( + $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr), + $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr), + ), + $nodeCallback, + )->getScope(); + } + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + ) { + $scope = $scope->assignVariable('http_response_header', TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()), new ArrayType(new IntegerType(), new StringType()), TrinaryLogic::createYes()); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' + ) { $arrayArg = $expr->getArgs()[0]->value; - $originalArrayType = $scope->getType($arrayArg); - $constantArrays = TypeUtils::getConstantArrays($originalArrayType); - if ( - $functionReflection->getName() === 'array_push' - || ($originalArrayType->isArray()->yes() && count($constantArrays) === 0) - ) { - $arrayType = $originalArrayType; - foreach ($argumentTypes as $argType) { - $arrayType = $arrayType->setOffsetValueType(null, $argType); - } - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); - } elseif (count($constantArrays) > 0) { - $defaultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($argumentTypes as $argType) { - $defaultArrayBuilder->setOffsetValueType(null, $argType); - } + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $arrayArg, + new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), + $nodeCallback, + )->getScope(); + } - $defaultArrayType = $defaultArrayBuilder->getArray(); + if ( + $functionReflection !== null + && $functionReflection->getName() === 'array_splice' + && count($expr->getArgs()) >= 2 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayType = $defaultArrayType; - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; - } - $arrayType = $arrayType->setOffsetValueType($keyType, $valueType); - } - $arrayTypes[] = $arrayType; - } + $offsetType = $scope->getType($expr->getArgs()[1]->value); + $lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType(); + $replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []); - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$arrayTypes) - ); - } + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $arrayArg, + new NativeTypeExpr( + $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), + ), + $nodeCallback, + )->getScope(); } if ( - isset($functionReflection) - && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + $functionReflection !== null + && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) + && count($expr->getArgs()) >= 1 ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); + $arrayArg = $expr->getArgs()[0]->value; + + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $arrayArg, + new NativeTypeExpr($this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg))), + $nodeCallback, + )->getScope(); } if ( - isset($functionReflection) - && $functionReflection->getName() === 'array_splice' + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; - $arrayArgType = $scope->getType($arrayArg); - $valueType = $arrayArgType->getIterableValueType(); - if (count($expr->getArgs()) >= 4) { - $valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType()); - } - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( + + $scope = $this->processVirtualAssign( + $scope, + $stmt, $arrayArg, - new ArrayType($arrayArgType->getIterableKeyType(), $valueType) - ); + new NativeTypeExpr($this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg))), + $nodeCallback, + )->getScope(); } - if (isset($functionReflection) && $functionReflection->getName() === 'extract') { - $scope = $scope->afterExtractCall(); + if ( + $functionReflection !== null + && $functionReflection->getName() === 'extract' + ) { + $extractedArg = $expr->getArgs()[0]->value; + $extractedType = $scope->getType($extractedArg); + $constantArrays = $extractedType->getConstantArrays(); + if (count($constantArrays) > 0) { + $properties = []; + $optionalProperties = []; + $refCount = []; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if ($keyType->isString()->no()) { + // integers as variable names not allowed + continue; + } + $key = (string) $keyType->getValue(); + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $key; + } + if (isset($properties[$key])) { + $properties[$key] = TypeCombinator::union($properties[$key], $valueType); + $refCount[$key]++; + } else { + $properties[$key] = $valueType; + $refCount[$key] = 1; + } + } + } + foreach ($properties as $name => $type) { + $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); + $scope = $scope->assignVariable($name, $type, $type, $optional ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes()); + } + } else { + $scope = $scope->afterExtractCall(); + } } - if (isset($functionReflection) && ($functionReflection->getName() === 'clearstatcache' || $functionReflection->getName() === 'unlink')) { + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) + ) { $scope = $scope->afterClearstatcacheCall(); } - if (isset($functionReflection) && $functionReflection->hasSideEffects()->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); - } + if ( + $functionReflection !== null + && str_starts_with($functionReflection->getName(), 'openssl') + ) { + $scope = $scope->afterOpenSslCall($functionReflection->getName()); } } elseif ($expr instanceof MethodCall) { @@ -1973,46 +2832,115 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression && strtolower($expr->name->name) === 'call' && isset($expr->getArgs()[0]) ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($expr->getArgs()[0]->value)); + $closureCallScope = $scope->enterClosureCall( + $scope->getType($expr->getArgs()[0]->value), + $scope->getNativeType($expr->getArgs()[0]->value), + ); } - $result = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } $parametersAcceptor = null; $methodReflection = null; + $calledOnType = $scope->getType($expr->var); if ($expr->name instanceof Expr) { - $methodNameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); $scope = $methodNameResult->getScope(); } else { - $calledOnType = $scope->getType($expr->var); $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); + $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); + } + + $result = $this->processArgs( + $stmt, + $methodReflection, + $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, + $parametersAcceptor, + $expr, + $scope, + $nodeCallback, + $context, + ); $scope = $result->getScope(); + if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { + $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + } + if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $scope = $scope->assignExpression( + $expr->var, + TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createCovariant(), + ), + $scope->getNativeType($expr->var), + ); + } + } + + if ( + $scope->isInClass() + && $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() + /*&& ( + // should not be allowed but in practice has to be + $scope->getClassReflection()->isFinal() + || $methodReflection->isFinal()->yes() + || $methodReflection->isPrivate() + )*/ + && TypeUtils::findThisType($calledOnType) !== null + ) { + $calledMethodScope = $this->processCalledMethod($methodReflection); + if ($calledMethodScope !== null) { + $scope = $scope->mergeInitializedProperties($calledMethodScope); } } } else { @@ -2020,40 +2948,44 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\NullsafeMethodCall) { - $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $expr->var); - $exprResult = $this->processExprNode(new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); + $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + $exprResult->getImpurePoints(), + static fn (): MutatingScope => $scope->filterByTruthyValue($expr), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr->class)); + $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType(new New_($expr->class))); + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); } if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { + $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { }, $context->enterDeep()); $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; } - $classResult = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -2063,418 +2995,613 @@ static function () use ($scope, $expr): MutatingScope { $parametersAcceptor = null; $methodReflection = null; if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr->class instanceof Name) { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { - $classReflection = $this->reflectionProvider->getClass($className); - if (is_string($expr->name)) { - $methodName = $expr->name; - } else { - $methodName = $expr->name->name; + $classType = $scope->resolveTypeByName($expr->class); + $methodName = $expr->name->name; + if ($classType->hasMethod($methodName)->yes()) { + $methodReflection = $classType->getMethod($methodName, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; } - if ($classReflection->hasMethod($methodName)) { - $methodReflection = $classReflection->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants() - ); - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; - } - if ( - $classReflection->getName() === 'Closure' - && strtolower($methodName) === 'bind' - ) { - $thisType = null; - if (isset($expr->getArgs()[1])) { - $argType = $scope->getType($expr->getArgs()[1]->value); - if ($argType instanceof NullType) { - $thisType = null; - } else { - $thisType = $argType; - } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $declaringClass->getName() === 'Closure' + && strtolower($methodName) === 'bind' + ) { + $thisType = null; + $nativeThisType = null; + if (isset($expr->getArgs()[1])) { + $argType = $scope->getType($expr->getArgs()[1]->value); + if ($argType->isNull()->yes()) { + $thisType = null; + } else { + $thisType = $argType; } - $scopeClass = 'static'; - if (isset($expr->getArgs()[2])) { - $argValue = $expr->getArgs()[2]->value; - $argValueType = $scope->getType($argValue); - - $directClassNames = TypeUtils::getDirectClassNames($argValueType); - if (count($directClassNames) === 1) { - $scopeClass = $directClassNames[0]; - $thisType = new ObjectType($scopeClass); - } elseif ( - $argValue instanceof Expr\ClassConstFetch - && $argValue->name instanceof Node\Identifier - && strtolower($argValue->name->name) === 'class' - && $argValue->class instanceof Name - ) { - $scopeClass = $scope->resolveName($argValue->class); - $thisType = new ObjectType($scopeClass); - } elseif ($argValueType instanceof ConstantStringType) { - $scopeClass = $argValueType->getValue(); - $thisType = new ObjectType($scopeClass); + + $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); } - } else { - $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); + } + $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); + if ( $methodReflection !== null - && !$methodReflection->isStatic() - && $methodReflection->hasSideEffects()->yes() - && $scopeFunction instanceof MethodReflection - && !$scopeFunction->isStatic() - && $scope->isInClass() && ( - $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() - || $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName()) + $methodReflection->hasSideEffects()->yes() + || ( + !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + ) ) + && $scope->isInClass() + && $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName()) ) { $scope = $scope->invalidateExpression(new Variable('this'), true); } - if ($methodReflection !== null) { - if ($methodReflection->hasSideEffects()->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + if ( + $methodReflection !== null + && !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + && $scopeFunction instanceof MethodReflection + && !$scopeFunction->isStatic() + && $scope->isInClass() + && $scope->getClassReflection()->isSubclassOfClass($methodReflection->getDeclaringClass()) + ) { + $thisType = $scope->getType(new Variable('this')); + $methodClassReflection = $methodReflection->getDeclaringClass(); + foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) { + continue; } + + $scope = $scope->assignInitializedProperty($thisType, $property->getName()); } } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); + if ($this->phpVersion->supportsPropertyHooks()) { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + } + } else { + $propertyName = $expr->name->toString(); + $propertyHolderType = $scopeBeforeVar->getType($expr->var); + $propertyReflection = $scopeBeforeVar->getInstancePropertyReflection($propertyHolderType, $propertyName); + if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + if ($propertyDeclaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $propertyDeclaringClass->getNativeProperty($propertyName); + $throwPoints = array_merge($throwPoints, $this->getPropertyReadThrowPointsFromGetHook($scopeBeforeVar, $expr, $nativeProperty)); + } + } } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { - $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $expr->var); - $exprResult = $this->processExprNode(new PropertyFetch($expr->var, $expr->name, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); + $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + $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, + ), + ]; + $isAlwaysTerminating = 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()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - return $this->processClosureNode($expr, $scope, $nodeCallback, $context, null); - } elseif ($expr instanceof Expr\ClosureUse) { - $this->processExprNode($expr->var, $scope, $nodeCallback, $context); - $hasYield = false; - $throwPoints = []; + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + + return new ExpressionResult( + $processClosureResult->getScope(), + false, + false, + [], + [], + ); } elseif ($expr instanceof Expr\ArrowFunction) { - return $this->processArrowFunctionNode($expr, $scope, $nodeCallback, $context, null); + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); + return new ExpressionResult( + $result->getScope(), + $result->hasYield(), + false, + [], + [], + ); } 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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), + ]; + $isAlwaysTerminating = 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 = []; + $isAlwaysTerminating = false; 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } } elseif ($expr instanceof ArrayDimFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Array_) { $itemNodes = []; $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); - if ($arrayItem === null) { - continue; + $nodeCallback($arrayItem, $scope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); + $scope = $keyResult->getScope(); } - $result = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); + $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); - } elseif ($expr instanceof ArrayItem) { - $hasYield = false; - $throwPoints = []; - if ($expr->key !== null) { - $result = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); + } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { + $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()); } - $result = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); - } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); - $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); - $nodeCallback(new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByTruthyValue($expr); - }, - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByFalseyValue($expr); - } + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), + static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), + static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); - $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); + $rightExprType = $rightResult->getScope()->getType($expr->right); + if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { + $leftMergedWithRightScope = $leftResult->getTruthyScope(); + } else { + $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + } - $nodeCallback(new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByTruthyValue($expr); - }, - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByFalseyValue($expr); - } + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), + static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), + static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); } elseif ($expr instanceof Coalesce) { - $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false); - - if ($expr->left instanceof PropertyFetch || $expr->left instanceof Expr\NullsafePropertyFetch) { - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left->var); - } elseif ($expr->left instanceof StaticPropertyFetch) { - if ($expr->left->class instanceof Expr) { - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left->class); - } else { - $scope = $nonNullabilityResult->getScope(); - } + $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left); + $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); + $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $nodeCallback, $context->enterDeep()); + $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); + + $rightScope = $scope->filterByFalseyValue($expr); + $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $nodeCallback, $context->enterDeep()); + $rightExprType = $scope->getType($expr->right); + if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { + $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); } else { - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left); + $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope()); } - $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - if ($expr->left instanceof PropertyFetch || $expr->left instanceof Expr\NullsafePropertyFetch) { - $scope = $this->lookForExitVariableAssign($scope, $expr->left->var); - } elseif ($expr->left instanceof StaticPropertyFetch) { - if ($expr->left->class instanceof Expr) { - $scope = $this->lookForExitVariableAssign($scope, $expr->left->class); - } - } else { - $scope = $this->lookForExitVariableAssign($scope, $expr->left); - } - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope()->mergeWith($scope); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $hasYield = $condResult->hasYield() || $rightResult->hasYield(); + $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); + $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); } elseif ($expr instanceof BinaryOp) { - $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\Include_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + true, + ); + $hasYield = $result->hasYield(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $hasYield = $result->hasYield(); + + $exprType = $scope->getType($expr->expr); + $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); + if ($toStringMethod !== null) { + if (!$toStringMethod->hasSideEffects()->no()) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), + $toStringMethod->isPure()->no(), + ); + } + } + $scope = $result->getScope(); } elseif ( $expr instanceof Expr\BitwiseNot || $expr instanceof Cast || $expr instanceof Expr\Clone_ - || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus ) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $hasYield = $result->hasYield(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\Eval_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true); $hasYield = $result->hasYield(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\YieldFrom) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'yieldFrom', + 'yield from', + true, + ); $hasYield = true; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\ClassConstFetch) { - $hasYield = false; - $throwPoints = []; + $isAlwaysTerminating = 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + } 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + } 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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 = []; + $isAlwaysTerminating = false; 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $nonNullabilityResults[] = $nonNullabilityResult; - $scope = $this->lookForExitVariableAssign($scope, $var); + } + foreach (array_reverse($expr->vars) as $var) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); } foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) { $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } } elseif ($expr instanceof Instanceof_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, []); + return new ExpressionResult($scope, false, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; $hasYield = false; $throwPoints = []; - if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr)); - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new New_(new Name($objectClasses[0])), $scope, static function (): void { - }, $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); + $impurePoints = []; + $isAlwaysTerminating = false; + $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } } else { - $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + $className = $scope->resolveName($expr->class); } - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } - } elseif ($expr->class instanceof Class_) { - $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name - $this->processStmtNode($expr->class, $scope, $nodeCallback); - } else { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { + $classReflection = null; + if ($className !== null && $this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $constructorReflection->getVariants() + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ); - $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; } @@ -2482,218 +3609,715 @@ static function () use ($expr, $rightResult): MutatingScope { } 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->getArgs(), $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ( $expr instanceof Expr\PreInc || $expr instanceof Expr\PostInc || $expr instanceof Expr\PreDec || $expr instanceof Expr\PostDec ) { - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); - $throwPoints = []; - 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); - 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->processVirtualAssign( + $scope, + $stmt, + $expr->var, + $newExpr, + $nodeCallback, + )->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(); + $isAlwaysTerminating = $ternaryCondResult->isAlwaysTerminating(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); - + $ifTrueType = null; if ($expr->if !== null) { - $ifResult = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context); + $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); + $ifTrueType = $ifTrueScope->getType($expr->if); } - $elseResult = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context); + $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + $condType = $scope->getType($expr->cond); + if ($condType->isTrue()->yes()) { + $finalScope = $ifTrueScope; + } elseif ($condType->isFalse()->yes()) { + $finalScope = $ifFalseScope; + } else { + if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $finalScope = $ifFalseScope; + } else { + $ifFalseType = $ifFalseScope->getType($expr->else); + + if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; + } else { + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + } + } + } return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), + $isAlwaysTerminating, $throwPoints, - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByTruthyValue($expr); - }, - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByFalseyValue($expr); - } + $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, + ), + ]; + $isAlwaysTerminating = false; if ($expr->key !== null) { - $keyResult = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $keyResult->isAlwaysTerminating(); } if ($expr->value !== null) { - $valueResult = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); + $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); } $hasYield = true; } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); - $condResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $deepContext); + $condType = $scope->getType($expr->cond); + $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); - $matchScope = $scope; + $impurePoints = $condResult->getImpurePoints(); + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); + $matchScope = $scope->enterMatch($expr); $armNodes = []; - foreach ($expr->arms as $arm) { + $hasDefaultCond = false; + $hasAlwaysTrueCond = false; + $arms = $expr->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $condNodes = []; + $conditionCases = []; + $conditionExprs = []; + foreach ($arm->conds as $cond) { + if (!$cond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$cond->class instanceof Name) { + continue 2; + } + if (!$cond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $scope->resolveName($cond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + if (!array_key_exists($loweredFetchedClassName, $unusedIndexedEnumCases)) { + throw new ShouldNotHappenException(); + } + + $caseName = $cond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $enumCase = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + $conditionCases[] = $enumCase; + $armConditionScope = $matchScope; + if (!array_key_exists($caseName, $unusedIndexedEnumCases[$loweredFetchedClassName])) { + // force "always false" + $armConditionScope = $armConditionScope->removeTypeFromExpression( + $expr->cond, + $enumCase, + ); + } else { + $unusedCasesCount = 0; + foreach ($unusedIndexedEnumCases as $cases) { + $unusedCasesCount += count($cases); + } + if ($unusedCasesCount === 1) { + $hasAlwaysTrueCond = true; + + // force "always true" + $armConditionScope = $armConditionScope->addTypeToExpression( + $expr->cond, + $enumCase, + ); + } + } + + $this->processExprNode($stmt, $cond, $armConditionScope, $nodeCallback, $deepContext); + + $condNodes[] = new MatchExpressionArmCondition( + $cond, + $armConditionScope, + $cond->getStartLine(), + ); + $conditionExprs[] = $cond; + + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $filteringExpr = $this->getFilteringExprForMatchArm($expr, $conditionExprs); + $matchArmBodyScope = $matchScope->addTypeToExpression( + $expr->cond, + $conditionCaseType, + )->filterByTruthyValue($filteringExpr); + $matchArmBody = new MatchExpressionArmBody($matchArmBodyScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); + + $armResult = $this->processExprNode( + $stmt, + $arm->body, + $matchArmBodyScope, + $nodeCallback, + ExpressionContext::createTopLevel(), + ); + $armScope = $armResult->getScope(); + $scope = $scope->mergeWith($armScope); + $hasYield = $hasYield || $armResult->hasYield(); + $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($expr->cond, $remainingType); + } + } + foreach ($arms as $i => $arm) { if ($arm->conds === null) { - $armResult = $this->processExprNode($arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasDefaultCond = true; + $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); + $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $scope = $scope->mergeWith($matchScope); - $armNodes[] = new MatchExpressionArm([], $arm->getLine()); continue; } if (count($arm->conds) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $filteringExpr = null; + $filteringExprs = []; $armCondScope = $matchScope; $condNodes = []; foreach ($arm->conds as $armCond) { - $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getLine()); - $armCondResult = $this->processExprNode($armCond, $armCondScope, $nodeCallback, $deepContext); + $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine()); + $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); - $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; + $armCondResultScope = $armCondResult->getScope(); + $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + if ($armCondType->isTrue()->yes()) { + $hasAlwaysTrueCond = true; } - - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); + $filteringExprs[] = $armCond; } - $armNodes[] = new MatchExpressionArm($condNodes, $arm->getLine()); + $filteringExpr = $this->getFilteringExprForMatchArm($expr, $filteringExprs); + + $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { + }, $deepContext)->getTruthyScope(); + $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); $armResult = $this->processExprNode( + $stmt, $arm->body, - $matchScope->filterByTruthyValue($filteringExpr), + $bodyScope, $nodeCallback, - ExpressionContext::createTopLevel() + 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); } - $nodeCallback(new MatchExpressionNode($expr->cond, $armNodes, $expr, $matchScope), $scope); + $remainingType = $matchScope->getType($expr->cond); + if (!$hasDefaultCond && !$hasAlwaysTrueCond && !$remainingType instanceof NeverType) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false); + } + + ksort($armNodes, SORT_NUMERIC); + + $nodeCallback(new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope); + } elseif ($expr instanceof AlwaysRememberedExpr) { + $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); + } elseif ($expr instanceof FunctionCallableNode) { + $throwPoints = []; + $impurePoints = []; + $hasYield = false; + $isAlwaysTerminating = 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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + } + } 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(); + $isAlwaysTerminating = false; + 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()); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); + } + } elseif ($expr instanceof StaticMethodCallableNode) { + $throwPoints = []; + $impurePoints = []; + $hasYield = false; + $isAlwaysTerminating = 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(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); + } + 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); + } + } elseif ($expr instanceof InstantiationCallableNode) { + $throwPoints = []; + $impurePoints = []; + $hasYield = false; + $isAlwaysTerminating = 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(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); + } + } elseif ($expr instanceof Node\Scalar) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + } elseif ($expr instanceof ConstFetch) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + $nodeCallback($expr->name, $scope); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; } return new ExpressionResult( $scope, $hasYield, + $isAlwaysTerminating, $throwPoints, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + $impurePoints, + static fn (): MutatingScope => $scope->filterByTruthyValue($expr), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } - private function getFunctionThrowPoint( - FunctionReflection $functionReflection, - ?ParametersAcceptor $parametersAcceptor, - FuncCall $funcCall, - MutatingScope $scope - ): ?ThrowPoint + private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) { - if (!$extension->isFunctionSupported($functionReflection)) { - continue; - } + $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; + } - $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $funcCall, $scope); - if ($throwType === null) { - return null; + $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); } + }; - return ThrowPoint::createExplicit($scope, $throwType, $funcCall, 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); - $throwType = $functionReflection->getThrowType(); - if ($throwType === null && $parametersAcceptor !== null) { - $returnType = $parametersAcceptor->getReturnType(); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { - $throwType = new ObjectType(\Throwable::class); - } - } + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { + $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); + }, + $nonConstantArrayWasUnpacked, + ); - if ($throwType !== null) { - if (!$throwType instanceof VoidType) { - 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; + 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), + ); } + } - $requiredParameters++; + $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 ( @@ -2703,7 +4327,7 @@ private function getFunctionThrowPoint( || count($funcCall->getArgs()) > 0 ) { $functionReturnedType = $scope->getType($funcCall); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $funcCall); } } @@ -2714,34 +4338,37 @@ private function getFunctionThrowPoint( private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { - if (!$extension->isMethodSupported($methodReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($methodReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } } $throwType = $methodReflection->getThrowType(); if ($throwType === null) { $returnType = $parametersAcceptor->getReturnType(); if ($returnType instanceof NeverType && $returnType->isExplicit()) { - $throwType = new ObjectType(\Throwable::class); + $throwType = new ObjectType(Throwable::class); } } if ($throwType !== null) { - if (!$throwType instanceof VoidType) { + 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()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2752,29 +4379,32 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet /** * @param Node\Arg[] $args */ - private function getConstructorThrowPoint(MethodReflection $constructorReflection, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint + private function getConstructorThrowPoint(MethodReflection $constructorReflection, ParametersAcceptor $parametersAcceptor, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint { $methodCall = new StaticCall($className, $constructorReflection->getName(), $args); - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { - if (!$extension->isStaticMethodSupported($constructorReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($constructorReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $new, false); + return ThrowPoint::createExplicit($scope, $throwType, $new, false); + } } if ($constructorReflection->getThrowType() !== null) { $throwType = $constructorReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $new, true); } } elseif ($this->implicitThrows) { - if ($classReflection->getName() !== \Throwable::class && !$classReflection->isSubclassOf(\Throwable::class)) { + if (!$classReflection->is(Throwable::class)) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2782,29 +4412,32 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return null; } - private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint + private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { - if (!$extension->isStaticMethodSupported($methodReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($methodReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } } if ($methodReflection->getThrowType() !== null) { $throwType = $methodReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($methodCall); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2813,7 +4446,83 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, S } /** - * @param Expr $expr + * @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 @@ -2826,7 +4535,7 @@ private function getAssignedVariables(Expr $expr): array return []; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { $names = []; foreach ($expr->items as $item) { if ($item === null) { @@ -2839,20 +4548,21 @@ private function getAssignedVariables(Expr $expr): array return $names; } + if ($expr instanceof ArrayDimFetch) { + return $this->getAssignedVariables($expr->var); + } + return []; } /** - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param Expr $expr - * @param MutatingScope $scope - * @param ExpressionContext $context + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function callNodeCallbackWithExpression( callable $nodeCallback, Expr $expr, MutatingScope $scope, - ExpressionContext $context + ExpressionContext $context, ): void { if ($context->isDeep()) { @@ -2862,36 +4572,30 @@ private function callNodeCallbackWithExpression( } /** - * @param \PhpParser\Node\Expr\Closure $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param Type|null $passedToType - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( + Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, 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) { @@ -2901,9 +4605,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; @@ -2915,10 +4621,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; } @@ -2932,15 +4648,44 @@ 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(); + } + $returnType = $closureType->getReturnType(); + $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit()); + + $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); + + $executionEnds = []; $gatheredReturnStatements = []; $gatheredYieldStatements = []; - $closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; } @@ -2950,150 +4695,232 @@ private function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }; + if (count($byRefUses) === 0) { - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, - $statementResult + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope, false, []); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } $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); - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, - $statementResult + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); + } + + /** + * @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 \PhpParser\Node\Expr\ArrowFunction $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param Type|null $passedToType - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArrowFunctionNode( + Node\Stmt $stmt, Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, - ExpressionContext $context, - ?Type $passedToType + ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($expr->returnType !== null) { $nodeCallback($expr->returnType, $scope); } - if ($passedToType !== null && !$passedToType->isCallable()->no()) { - $callableParameters = null; - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - } - } else { - $callableParameters = null; + $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters( + $scope, + $expr, + $arrowFunctionCallArgs, + $passedToType, + )); + $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); + if (!$arrowFunctionType instanceof ClosureType) { + throw new ShouldNotHappenException(); } + $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); - $nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope); - $this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - - return new ExpressionResult($scope, false, []); + return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } - private function lookForArrayDestructuringArray(MutatingScope $scope, Expr $expr, Type $valueType): MutatingScope + /** + * @param Node\Arg[] $args + * @return ParameterReflection[]|null + */ + public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array { - 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; + $callableParameters = null; + if ($args !== null) { + $closureType = $scope->getType($closureExpr); + + if ($closureType->isCallable()->no()) { + return null; + } + + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; + } + + $type = $scope->getType($args[$index]->value); + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); } + } + } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { + if ($passedToType instanceof UnionType) { + $passedToType = TypeCombinator::union(...array_filter( + $passedToType->getTypes(), + static fn (Type $type) => $type->isCallable()->yes(), + )); - $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 callable(\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, - callable $nodeCallback + callable $nodeCallback, ): void { - foreach ($param->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } + $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback); $nodeCallback($param, $scope); if ($param->type !== null) { $nodeCallback($param->type, $scope); @@ -3102,127 +4929,657 @@ private function processParamNode( return; } - $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); } /** - * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection|null $calleeReflection - * @param ParametersAcceptor|null $parametersAcceptor - * @param \PhpParser\Node\Arg[] $args - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \PHPStan\Analyser\MutatingScope|null $closureBindScope - * @return \PHPStan\Analyser\ExpressionResult + * @param AttributeGroup[] $attrGroups + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processArgs( - $calleeReflection, - ?ParametersAcceptor $parametersAcceptor, - array $args, + private function processAttributeGroups( + Node\Stmt $stmt, + array $attrGroups, MutatingScope $scope, callable $nodeCallback, - ExpressionContext $context, - ?MutatingScope $closureBindScope = null - ): ExpressionResult + ): void { - if ($parametersAcceptor !== null) { - $parameters = $parametersAcceptor->getParameters(); + 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); } + } - if ($calleeReflection !== null) { - $scope = $scope->pushInFunctionCall($calleeReflection); + /** + * @param FunctionReflection|MethodReflection|null $calleeReflection + */ + public function resolveClosureThisType( + ?CallLike $call, + $calleeReflection, + ParameterReflection $parameter, + MutatingScope $scope, + ): ?Type + { + if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) { + if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) { + if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) { + if (! $extension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + if ($parameter instanceof ExtendedParameterReflection) { + return $parameter->getClosureThisType(); + } + + return null; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processArgs( + Node\Stmt $stmt, + $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, + ?ParametersAcceptor $parametersAcceptor, + CallLike $callLike, + MutatingScope $scope, + callable $nodeCallback, + ExpressionContext $context, + ?MutatingScope $closureBindScope = null, + ): ExpressionResult + { + $args = $callLike->getArgs(); + + if ($parametersAcceptor !== null) { + $parameters = $parametersAcceptor->getParameters(); } $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; foreach ($args as $i => $arg) { - $nodeCallback($arg, $scope); + $assignByReference = false; + $parameter = null; + $parameterType = null; + $parameterNativeType = null; if (isset($parameters) && $parametersAcceptor !== null) { - $assignByReference = false; if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + + if ($parameters[$i] instanceof ExtendedParameterReflection) { + $parameterNativeType = $parameters[$i]->getNativeType(); + } + $parameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); - } - if ($assignByReference) { - $argValue = $arg->value; - if ($argValue instanceof Variable && is_string($argValue->name)) { - $scope = $scope->assignVariable($argValue->name, new MixedType()); + if ($lastParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $lastParameter->getNativeType(); } + $parameter = $lastParameter; + } + } + + $lookForUnset = false; + if ($assignByReference) { + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) + ) { + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; } } + if ($calleeReflection !== null) { + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter); + } + + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $nodeCallback($originalArg, $scope); + $originalScope = $scope; $scopeToPass = $scope; if ($i === 0 && $closureBindScope !== null) { $scopeToPass = $closureBindScope; } + $parameterCallableType = null; + if ($parameterType !== null) { + $parameterCallableType = TypeUtils::findCallableType($parameterType); + } + + if ($parameter instanceof ExtendedParameterReflection) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; + } + if ($arg->value instanceof Expr\Closure) { + $restoreThisScope = null; + if ( + $closureBindScope === null + && $parameter instanceof ExtendedParameterReflection + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processClosureNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating(); + } + + $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 + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processArrowFunctionNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $arrowFunctionResult->isAlwaysTerminating(); + } } 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()); + $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); + $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())); + $returnType = $acceptors[0]->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); + } + } + } } - $scope = $result->getScope(); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + if ($i !== 0 || $closureBindScope === null) { continue; } $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } + foreach ($args as $i => $arg) { + if (!isset($parameters) || $parametersAcceptor === null) { + continue; + } + + $byRefType = new MixedType(); + $assignByReference = false; + $currentParameter = null; + if (isset($parameters[$i])) { + $currentParameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $currentParameter = $parameters[count($parameters) - 1]; + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + if ($assignByReference) { + if ($currentParameter instanceof ExtendedParameterReflection && $currentParameter->getOutType() !== null) { + $byRefType = $currentParameter->getOutType(); + } elseif ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } elseif ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } + } + } + + if ($assignByReference) { + if ($currentParameter === null) { + throw new ShouldNotHappenException(); + } + + $argValue = $arg->value; + if (!$argValue instanceof Variable || $argValue->name !== 'this') { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + $byRefType = $paramOutType; + } + + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $argValue, + new TypeExpr($byRefType), + $nodeCallback, + )->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, $isAlwaysTerminating, $throwPoints, $impurePoints); + } - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); + /** + * @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 new ExpressionResult($scope, $hasYield, $throwPoints); + return null; } /** - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \PhpParser\Node\Expr $var - * @param \PhpParser\Node\Expr $assignedExpr - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \Closure(MutatingScope $scope): ExpressionResult $processExprCallback - * @param bool $enterExpressionAssign - * @return ExpressionResult + * @param 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 (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); + } + + return null; + } + + /** + * @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, callable $nodeCallback, ExpressionContext $context, - \Closure $processExprCallback, - bool $enterExpressionAssign + Closure $processExprCallback, + bool $enterExpressionAssign, ): ExpressionResult { $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); $hasYield = false; $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true); + } $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scope->getType($assignedExpr); - $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); $conditionalExpressions = []; + if ($assignedExpr instanceof Ternary) { + $if = $assignedExpr->if; + if ($if === null) { + $if = $assignedExpr->cond; + } + $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, static function (): void { + }, ExpressionContext::createDeep())->getScope(); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); + $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); + $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes); + $truthyType = $truthyScope->getType($if); + $falseyType = $falsyScope->getType($assignedExpr->else); + + if ( + $truthyType->isSuperTypeOf($falseyType)->no() + && $falseyType->isSuperTypeOf($truthyType)->no() + ) { + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + } + } + + $scopeBeforeAssignEval = $scope; + $scope = $result->getScope(); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $truthyType = TypeCombinator::remove($type, StaticTypeFactory::falsey()); + $truthyType = TypeCombinator::removeFalsey($type); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); @@ -3230,43 +5587,89 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $scope = $result->getScope()->assignVariable($var->name, $type); + $nodeCallback(new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval); + $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; + } + + if ( + $var === $originalVar + && $var->dim !== null + && $scope->hasExpressionType($var)->yes() + ) { + $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( + $varForSetOffsetValue, + $var->dim, + $assignedPropertyExpr, + ); + } else { + $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(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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(); @@ -3278,130 +5681,584 @@ private function processAssignVar( } $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); $originalValueToWrite = $valueToWrite; + $originalNativeValueToWrite = $nativeValueToWrite; + $scopeBeforeAssignEval = $scope; // 3. eval assigned expr $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $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 + $isImplicitArrayCreation = $this->isImplicitArrayCreation($dimFetchStack, $scope); + if ($isImplicitArrayCreation->yes()) { + $varType = new ConstantArrayType([], []); + $varNativeType = new ConstantArrayType([], []); + } + $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; + + [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + + if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { + [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + } else { + $rewritten = false; + foreach ($offsetTypes as $i => $offsetType) { + $offsetNativeType = $offsetNativeTypes[$i]; + + if ($offsetType === null) { + if ($offsetNativeType !== null) { + throw new ShouldNotHappenException(); } + + continue; + } elseif ($offsetNativeType === null) { + throw new ShouldNotHappenException(); + } + if ($offsetType->equals($offsetNativeType)) { + continue; } - $offsetValueTypeStack[] = $offsetValueType; + [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + $rewritten = true; + break; } - foreach (array_reverse($offsetTypes) as $i => $offsetType) { - /** @var Type $offsetValueType */ - $offsetValueType = array_pop($offsetValueTypeStack); - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + if (!$rewritten) { + $nativeValueToWrite = $valueToWrite; } + } + if ($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), $scopeBeforeAssignEval); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval); + 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); - if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) { - $scope = $scope->assignExpression( - $originalVar, - $originalValueToWrite + 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), $scopeBeforeAssignEval); + } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } + } + } + + foreach ($additionalExpressions as $k => $additionalExpression) { + [$expr, $type] = $additionalExpression; + $nativeType = $type; + if (isset($additionalNativeExpressions[$k])) { + [, $nativeType] = $additionalNativeExpressions[$k]; + } + + $scope = $scope->assignExpression($expr, $type, $nativeType); + } + + 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) { + $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); + $hasYield = $objectResult->hasYield(); + $throwPoints = $objectResult->getThrowPoints(); + $impurePoints = $objectResult->getImpurePoints(); + $isAlwaysTerminating = $objectResult->isAlwaysTerminating(); + $scope = $objectResult->getScope(); + + $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $propertyNameResult->isAlwaysTerminating(); + $scope = $propertyNameResult->getScope(); + } + + $scopeBeforeAssignEval = $scope; + $result = $processExprCallback($scope); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + $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->hasInstanceProperty($propertyName)->yes()) { + $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + if ($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)); + } + } + $declaringClass = $propertyReflection->getDeclaringClass(); + if ($declaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $declaringClass->getNativeProperty($propertyName); + if ( + !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); + } + if ($this->phpVersion->supportsPropertyHooks()) { + $throwPoints = array_merge($throwPoints, $this->getPropertyAssignThrowPointsFromSetHook($scope, $var, $nativeProperty)); + } + if ($enterExpressionAssign) { + $scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName); + } + } + } else { + // fallback + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $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 Node\Name) { + $propertyHolderType = $scope->resolveTypeByName($var->class); + } else { + $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context); + $propertyHolderType = $scope->getType($var->class); + } + + $propertyName = null; + if ($var->name instanceof Node\Identifier) { + $propertyName = $var->name->name; + } else { + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); + $hasYield = $propertyNameResult->hasYield(); + $throwPoints = $propertyNameResult->getThrowPoints(); + $impurePoints = $propertyNameResult->getImpurePoints(); + $isAlwaysTerminating = $propertyNameResult->isAlwaysTerminating(); + $scope = $propertyNameResult->getScope(); + } + + $scopeBeforeAssignEval = $scope; + $result = $processExprCallback($scope); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + $scope = $result->getScope(); + + if ($propertyName !== null) { + $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + 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 + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $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()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); + $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()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); + + 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, false, [], []), + $enterExpressionAssign, + ); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + } + } 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), $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, $isAlwaysTerminating, $throwPoints, $impurePoints); + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processVirtualAssign(MutatingScope $scope, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult + { + return $this->processAssignVar( + $scope, + $stmt, + $var, + $assignedExpr, + 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, [], []), + false, + ); + } + + /** + * @param list $dimFetchStack + */ + private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): TrinaryLogic + { + if (count($dimFetchStack) === 0) { + return TrinaryLogic::createNo(); + } + + $varNode = $dimFetchStack[0]->var; + if (!$varNode instanceof Variable) { + return TrinaryLogic::createNo(); + } + + if (!is_string($varNode->name)) { + return TrinaryLogic::createNo(); + } + + return $scope->hasVariableType($varNode->name)->negate(); + } + + /** + * @param list $dimFetchStack + * @param list $offsetTypes + * + * @return array{Type, list} + */ + private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array + { + $originalValueToWrite = $valueToWrite; + + $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)); + } + + $arrayDimFetch = $dimFetchStack[$i] ?? null; + if ( + $offsetType !== null + && $arrayDimFetch !== null + && $scope->hasExpressionType($arrayDimFetch)->yes() + ) { + $hasOffsetType = null; + if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { + $hasOffsetType = new HasOffsetValueType($offsetType, $valueToWrite); + } + $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + + if ($valueToWrite->isArray()->yes()) { + if ($hasOffsetType !== null) { + $valueToWrite = TypeCombinator::intersect( + $valueToWrite, + $hasOffsetType, + ); + } else { + $valueToWrite = TypeCombinator::intersect( + $valueToWrite, + new NonEmptyArrayType(), ); } } - } - } elseif ($var instanceof PropertyFetch) { - $objectResult = $this->processExprNode($var->var, $scope, $nodeCallback, $context); - $hasYield = $objectResult->hasYield(); - $throwPoints = $objectResult->getThrowPoints(); - $scope = $objectResult->getScope(); - $propertyName = null; - if ($var->name instanceof Node\Identifier) { - $propertyName = $var->name->name; } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); - $hasYield = $hasYield || $propertyNameResult->hasYield(); - $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); - $scope = $propertyNameResult->getScope(); + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); } - $result = $processExprCallback($scope); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); - - $propertyHolderType = $scope->getType($var->var); - if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) { - $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope); - if ($propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); - } - } else { - // fallback - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { + continue; } - } elseif ($var instanceof Expr\StaticPropertyFetch) { - if ($var->class instanceof \PhpParser\Node\Name) { - $propertyHolderType = $scope->resolveTypeByName($var->class); - } else { - $this->processExprNode($var->class, $scope, $nodeCallback, $context); - $propertyHolderType = $scope->getType($var->class); + if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) { + continue; } - $propertyName = null; - if ($var->name instanceof Node\Identifier) { - $propertyName = $var->name->name; - $hasYield = false; - $throwPoints = []; - } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); - $hasYield = $propertyNameResult->hasYield(); - $throwPoints = $propertyNameResult->getThrowPoints(); - $scope = $propertyNameResult->getScope(); + 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()); } + } - $result = $processExprCallback($scope); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); + $additionalExpressions = []; + $offsetValueType = $valueToWrite; + $lastDimKey = array_key_last($dimFetchStack); + foreach ($dimFetchStack as $key => $dimFetch) { + if ($dimFetch->dim === null) { + $additionalExpressions = []; + break; + } - if ($propertyName !== null) { - $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName); - if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); - } + if ($key === $lastDimKey) { + $offsetValueType = $originalValueToWrite; } else { - // fallback - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $offsetType = $scope->getType($dimFetch->dim); + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } + + $additionalExpressions[] = [$dimFetch, $offsetValueType]; } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return [$valueToWrite, $additionalExpressions]; } private function unwrapAssign(Expr $expr): Expr @@ -3414,11 +6271,7 @@ private function unwrapAssign(Expr $expr): Expr } /** - * @param Scope $scope - * @param string $variableName * @param array $conditionalExpressions - * @param SpecifiedTypes $specifiedTypes - * @param Type $variableType * @return array */ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array @@ -3431,26 +6284,28 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } - $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( - TypeCombinator::intersect($scope->getType($expr), $exprType) + $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 Scope $scope - * @param string $variableName * @param array $conditionalExpressions - * @param SpecifiedTypes $specifiedTypes - * @param Type $variableType * @return array */ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array @@ -3463,21 +6318,30 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } - $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( - TypeCombinator::remove($scope->getType($expr), $exprType) + $holder = new ConditionalExpressionHolder([ + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), + ], ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::remove($scope->getType($expr), $exprType), )); + $conditionalExpressions[$exprString][$holder->getKey()] = $holder; } return $conditionalExpressions; } - private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr): MutatingScope + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope { $function = $scope->getFunction(); $variableLessTags = []; @@ -3492,7 +6356,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); $assignedVariable = null; @@ -3528,25 +6392,37 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $certainty = TrinaryLogic::createYes(); } - $scope = $scope->assignVariable($name, $varTag->getType(), $certainty); + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope); + } + + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + $scope->getNativeType($variableNode), + $certainty, + ); } } if (count($variableLessTags) === 1 && $defaultExpr !== null) { - $scope = $scope->specifyExpressionType($defaultExpr, $variableLessTags[0]->getType()); + $originalType = $scope->getType($defaultExpr); + $varTag = $variableLessTags[0]; + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope); + } + $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType()); } return $scope; } /** - * @param MutatingScope $scope * @param array $variableNames - * @param Node $node - * @param bool $changed - * @return MutatingScope */ - private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node $node, bool &$changed = false): MutatingScope + private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope { $function = $scope->getFunction(); $varTags = []; @@ -3560,7 +6436,7 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { $varTags[$key] = $varTag; @@ -3578,86 +6454,145 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $variableType = $varTags[$variableName]->getType(); $changed = true; - $scope = $scope->assignVariable($variableName, $variableType); + $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes()); } if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { $variableType = $varTags[0]->getType(); $changed = true; - $scope = $scope->assignVariable($variableNames[0], $variableType); + $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes()); } return $scope; } - private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $scope->getType($stmt->expr); - $vars = []; - if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) { + + $iterateeType = $originalScope->getType($stmt->expr); + if ( + ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) + && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) + ) { + $keyVarName = null; + if ($stmt->keyVar 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, ); - $vars[] = $stmt->valueVar->name; - } - - if ( - $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) - ) { - $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); - $vars[] = $stmt->keyVar->name; + $vars = [$stmt->valueVar->name]; + if ($keyVarName !== null) { + $vars[] = $keyVarName; + } + } else { + $scope = $this->processVirtualAssign( + $scope, + $stmt, + $stmt->valueVar, + new GetIterableValueTypeExpr($stmt->expr), + $nodeCallback, + )->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->processVirtualAssign( + $scope, + $stmt, + $stmt->keyVar, + new GetIterableKeyTypeExpr($stmt->expr), + $nodeCallback, + )->getScope(); + $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); + } } + $constantArrays = $iterateeType->getConstantArrays(); if ( $stmt->getDocComment() === null - && $iterateeType instanceof ConstantArrayType + && $iterateeType->isConstantArray()->yes() + && count($constantArrays) === 1 && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $conditionalHolders = []; - foreach ($iterateeType->getKeyTypes() as $i => $keyType) { - $valueType = $iterateeType->getValueTypes()[$i]; - $conditionalHolders[] = new ConditionalExpressionHolder([ - '$' . $stmt->keyVar->name => $keyType, - ], new VariableTypeHolder($valueType, TrinaryLogic::createYes())); + $valueConditionalHolders = []; + $arrayDimFetchConditionalHolders = []; + foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) { + $valueType = $constantArrays[0]->getValueTypes()[$i]; + $holder = new ConditionalExpressionHolder([ + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder($stmt->valueVar, $valueType, TrinaryLogic::createYes())); + $valueConditionalHolders[$holder->getKey()] = $holder; + $arrayDimFetchHolder = new ConditionalExpressionHolder([ + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType, TrinaryLogic::createYes())); + $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder; } $scope = $scope->addConditionalExpressions( '$' . $stmt->valueVar->name, - $conditionalHolders + $valueConditionalHolders, ); + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $scope = $scope->addConditionalExpressions( + sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name), + $arrayDimFetchConditionalHolders, + ); + } } - if ($stmt->valueVar instanceof List_ || $stmt->valueVar instanceof Array_) { - $exprType = $scope->getType($stmt->expr); - $itemType = $exprType->getIterableValueType(); - $scope = $this->lookForArrayDestructuringArray($scope, $stmt->valueVar, $itemType); - $vars = array_merge($vars, $this->getAssignedVariables($stmt->valueVar)); + if ( + $stmt->expr instanceof FuncCall + && $stmt->expr->name instanceof Name + && $stmt->expr->name->toLowerString() === 'array_keys' + && $stmt->valueVar instanceof Variable + ) { + $args = $stmt->expr->getArgs(); + if (count($args) >= 1) { + $arrayArg = $args[0]->value; + $scope = $scope->assignExpression( + new ArrayDimFetch($arrayArg, $stmt->valueVar), + $scope->getType($arrayArg)->getIterableValueType(), + $scope->getNativeType($arrayArg)->getIterableValueType(), + ); + } } - $scope = $this->processVarAnnotation($scope, $vars, $stmt); - - return $scope; + return $this->processVarAnnotation($scope, $vars, $stmt); } /** - * @param \PhpParser\Node\Stmt\TraitUse $node - * @param MutatingScope $classScope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void { + $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; } @@ -3670,33 +6605,49 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS if (!isset($this->analysedFiles[$fileName])) { continue; } + $adaptations = []; + foreach ($node->adaptations as $adaptation) { + if ($adaptation->trait === null) { + $adaptations[] = $adaptation; + continue; + } + if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) { + continue; + } + + $adaptations[] = $adaptation; + } $parserNodes = $this->parser->parseFile($fileName); - $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $node->adaptations, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback); } } /** - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param ClassReflection $traitReflection - * @param \PHPStan\Analyser\MutatingScope $scope + * @param Node[]|Node|scalar|null $node * @param Node\Stmt\TraitUseAdaptation[] $adaptations - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @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; } - if ($adaptation->newModifier === null) { + $methodName = $adaptation->method->toLowerString(); + if ($adaptation->newModifier !== null) { + $methodModifiers[$methodName] = $adaptation->newModifier; + } + + if ($adaptation->newName === null) { continue; } - $methodModifiers[$adaptation->method->toLowerString()] = $adaptation->newModifier; + $methodNames[$methodName] = $adaptation->newName; } $stmts = $node->stmts; @@ -3705,15 +6656,26 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection continue; } $methodName = $stmt->name->toLowerString(); - if (!array_key_exists($methodName, $methodModifiers)) { + $methodAst = clone $stmt; + $stmts[$i] = $methodAst; + if (array_key_exists($methodName, $methodModifiers)) { + $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName]; + } + + if (!array_key_exists($methodName, $methodNames)) { continue; } - $methodAst = clone $stmt; - $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; - $stmts[$i] = $methodAst; + $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString()); + $methodAst->name = $methodNames[$methodName]; } - $this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback); + + 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) { @@ -3733,24 +6695,163 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } } + private function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope + { + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->isAnonymous()) { + return null; + } + if ($declaringClass->getFileName() === null) { + return null; + } + + $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName()); + if (array_key_exists($stackName, $this->calledMethodResults)) { + return $this->calledMethodResults[$stackName]; + } + + if (array_key_exists($stackName, $this->calledMethodStack)) { + return null; + } + + if (count($this->calledMethodStack) > 0) { + return null; + } + + $this->calledMethodStack[$stackName] = true; + + $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName()); + if (!isset($this->analysedFiles[$fileName])) { + return null; + } + $parserNodes = $this->parser->parseFile($fileName); + + $returnStatement = null; + $this->processNodesForCalledMethod($parserNodes, $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void { + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + + if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) { + return; + } + + if ($returnStatement !== null) { + return; + } + + $returnStatement = $node; + }); + + $calledMethodEndScope = null; + if ($returnStatement !== null) { + foreach ($returnStatement->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statementResult->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope()); + } + foreach ($returnStatement->getReturnStatements() as $statement) { + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statement->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope()); + } + } + + unset($this->calledMethodStack[$stackName]); + + $this->calledMethodResults[$stackName] = $calledMethodEndScope; + + return $calledMethodEndScope; + } + + /** + * @param Node[]|Node|scalar|null $node + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void + { + if ($node instanceof Node) { + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $node instanceof Node\Stmt\Class_ + && isset($node->namespacedName) + && $declaringClass->getName() === (string) $node->namespacedName + && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine() + ) { + + $stmts = $node->stmts; + foreach ($stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + + if ($stmt->name->toString() !== $methodReflection->getName()) { + continue; + } + + if ($stmt->getEndLine() - $stmt->getStartLine() > 50) { + continue; + } + + $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass); + $this->processStmtNode($stmt, $scope, $nodeCallback, StatementContext::createTopLevel()); + } + return; + } + if ($node instanceof Node\Stmt\ClassLike) { + return; + } + if ($node instanceof Node\FunctionLike) { + return; + } + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->{$subNodeName}; + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } elseif (is_array($node)) { + foreach ($node as $subNode) { + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } + } + /** - * @param Scope $scope - * @param Node\FunctionLike $functionLike - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} */ - public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array + public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; + $phpDocImmediatelyInvokedCallableParameters = []; + $phpDocClosureThisTypeParameters = []; $phpDocReturnType = null; $phpDocThrowType = null; $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; $isFinal = false; - $isPure = false; - $docComment = $functionLike->getDocComment() !== null - ? $functionLike->getDocComment()->getText() + $isPure = null; + $isAllowedPrivateMutation = false; + $acceptsNamedArguments = true; + $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); + $asserts = Assertions::createEmpty(); + $selfOutType = null; + $docComment = $node->getDocComment() !== null + ? $node->getDocComment()->getText() : null; $file = $scope->getFile(); @@ -3758,30 +6859,31 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null; $resolvedPhpDoc = null; $functionName = null; + $phpDocParameterOutTypes = []; - if ($functionLike instanceof Node\Stmt\ClassMethod) { + if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { - throw new \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, ); - if ($functionLike->name->toLowerString() === '__construct') { - foreach ($functionLike->params as $param) { + if ($node->name->toLowerString() === '__construct') { + foreach ($node->params as $param) { if ($param->flags === 0) { continue; } @@ -3794,7 +6896,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array !$param->var instanceof Variable || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( @@ -3802,7 +6904,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $class, $trait, '__construct', - $param->getDocComment()->getText() + $param->getDocComment()->getText(), ); $varTags = $paramPhpDoc->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -3816,8 +6918,13 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $phpDocParameterTypes[$param->var->name] = $phpDocType; } } - } elseif ($functionLike instanceof Node\Stmt\Function_) { - $functionName = trim($scope->getNamespace() . '\\' . $functionLike->name->name, '\\'); + } elseif ($node instanceof Node\Stmt\Function_) { + $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } if ($docComment !== null && $resolvedPhpDoc === null) { @@ -3826,12 +6933,14 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $class, $trait, $functionName, - $docComment + $docComment, ); } + $varTags = []; if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -3842,10 +6951,26 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array } $phpDocParameterTypes[$paramName] = $paramType; } - $nativeReturnType = $scope->getFunctionType($functionLike->getReturnType(), false, false); - $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); - if ($phpDocReturnType !== null && $scope->isInClass()) { - $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType); + foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) { + if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) { + continue; + } + $paramClosureThisType = $paramClosureThisTag->getType(); + if ($scope->isInClass()) { + $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType); + } + $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; + } + + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); + } + if ($node instanceof Node\FunctionLike) { + $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false); + $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); + if ($phpDocReturnType !== null && $scope->isInClass()) { + $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType); + } } $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; @@ -3853,9 +6978,18 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + if ($acceptsNamedArguments && $scope->isInClass()) { + $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments(); + } + $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + $varTags = $resolvedPhpDoc->getVarTags(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type @@ -3863,7 +6997,7 @@ private function transformStaticType(ClassReflection $declaringClass, Type $type return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { if ($type instanceof StaticType) { $changedType = $type->changeBaseClass($declaringClass); - if ($declaringClass->isFinal()) { + if ($declaringClass->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); @@ -3891,7 +7025,76 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ return $phpDocReturnType; } + if ($phpDocReturnType instanceof UnionType) { + $types = []; + foreach ($phpDocReturnType->getTypes() as $innerType) { + if (!$nativeReturnType->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $types[] = $innerType; + } + + if (count($types) === 0) { + return null; + } + + return TypeCombinator::union(...$types); + } + return null; } + /** + * @param array $nodes + * @return list + */ + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array + { + $stmts = []; + $isPassedUnreachableStatement = false; + foreach ($nodes as $node) { + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + continue; + } + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } + if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { + continue; + } + if (!$node instanceof Node\Stmt) { + continue; + } + $stmts[] = $node; + $isPassedUnreachableStatement = true; + } + return $stmts; + } + + /** + * @param array $conditions + */ + public function getFilteringExprForMatchArm(Expr\Match_ $expr, array $conditions): BinaryOp\Identical|FuncCall + { + if (count($conditions) === 1) { + return new BinaryOp\Identical($expr->cond, $conditions[0]); + } + + $items = []; + foreach ($conditions as $filteringExpr) { + $items[] = new Node\ArrayItem($filteringExpr); + } + + return new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($expr->cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); + } + } diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index a74dd6e426..0b439191c7 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -3,10 +3,25 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\Type\TypeCombinator; -class NullsafeOperatorHelper +final class NullsafeOperatorHelper { + public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr + { + if (!TypeCombinator::containsNull($scope->getType($expr))) { + // We're in most likely in context of a null-safe operator ($scope->moreSpecificType is defined for $expr) + // Modifying the expression would not bring any value or worse ruin the context information + return $expr; + } + + return self::getNullsafeShortcircuitedExpr($expr); + } + + /** + * @internal Use NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope + */ public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr { if ($expr instanceof Expr\NullsafeMethodCall) { diff --git a/src/Analyser/OutOfClassScope.php b/src/Analyser/OutOfClassScope.php index d9ddf23f2f..a2215bc25c 100644 --- a/src/Analyser/OutOfClassScope.php +++ b/src/Analyser/OutOfClassScope.php @@ -2,13 +2,14 @@ namespace PHPStan\Analyser; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\PropertyReflection; -class OutOfClassScope implements ClassMemberAccessAnswerer +final class OutOfClassScope implements ClassMemberAccessAnswerer { /** @api */ @@ -31,12 +32,24 @@ public function canAccessProperty(PropertyReflection $propertyReflection): bool return $propertyReflection->isPublic(); } + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic(); + } + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic() + && !$propertyReflection->isProtectedSet() + && !$propertyReflection->isPrivateSet(); + } + public function canCallMethod(MethodReflection $methodReflection): bool { return $methodReflection->isPublic(); } - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $constantReflection->isPublic(); } diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php new file mode 100644 index 0000000000..a133bfa77b --- /dev/null +++ b/src/Analyser/ProcessClosureResult.php @@ -0,0 +1,59 @@ +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; + } + + public function isAlwaysTerminating(): bool + { + return $this->isAlwaysTerminating; + } + +} diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index d18cb4a449..5cfefc6f46 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -3,56 +3,48 @@ namespace PHPStan\Analyser\ResultCache; use PHPStan\Analyser\Error; -use PHPStan\Dependency\ExportedNode; - -class ResultCache +use PHPStan\Analyser\FileAnalyserResult; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; + +/** + * @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 mixed[] */ - private array $meta; - - /** @var array> */ - private array $errors; - - /** @var array> */ - private array $dependencies; - - /** @var array> */ - private array $exportedNodes; - /** * @param string[] $filesToAnalyse - * @param bool $fullAnalysis - * @param int $lastFullAnalysisTime * @param mixed[] $meta - * @param array> $errors + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param CollectorData $collectedData * @param array> $dependencies - * @param array> $exportedNodes + * @param array> $usedTraitDependencies + * @param array> $exportedNodes + * @param array $projectExtensionFiles + * @param array $currentFileHashes */ public function __construct( - array $filesToAnalyse, - bool $fullAnalysis, - int $lastFullAnalysisTime, - array $meta, - array $errors, - array $dependencies, - array $exportedNodes + private array $filesToAnalyse, + private bool $fullAnalysis, + private int $lastFullAnalysisTime, + private array $meta, + private array $errors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + private array $collectedData, + private array $dependencies, + private array $usedTraitDependencies, + private array $exportedNodes, + private array $projectExtensionFiles, + private array $currentFileHashes, ) { - $this->filesToAnalyse = $filesToAnalyse; - $this->fullAnalysis = $fullAnalysis; - $this->lastFullAnalysisTime = $lastFullAnalysisTime; - $this->meta = $meta; - $this->errors = $errors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; } /** @@ -82,13 +74,45 @@ public function getMeta(): array } /** - * @return array> + * @return array> */ public function getErrors(): array { return $this->errors; } + /** + * @return array> + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + /** * @return array> */ @@ -98,11 +122,35 @@ public function getDependencies(): array } /** - * @return array> + * @return array> + */ + public function getUsedTraitDependencies(): array + { + return $this->usedTraitDependencies; + } + + /** + * @return array> */ public function getExportedNodes(): array { return $this->exportedNodes; } + /** + * @return array + */ + public function getProjectExtensionFiles(): array + { + return $this->projectExtensionFiles; + } + + /** + * @return array + */ + public function getCurrentFileHashes(): array + { + return $this->currentFileHashes; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheClearer.php b/src/Analyser/ResultCache/ResultCacheClearer.php index e6628002bb..8f1ce5a91c 100644 --- a/src/Analyser/ResultCache/ResultCacheClearer.php +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -2,19 +2,21 @@ namespace PHPStan\Analyser\ResultCache; -use Symfony\Component\Finder\Finder; - -class ResultCacheClearer +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use function dirname; +use function is_file; +use function unlink; + +#[AutowiredService] +final class ResultCacheClearer { - private string $cacheFilePath; - - private string $tempResultCachePath; - - public function __construct(string $cacheFilePath, string $tempResultCachePath) + public function __construct( + #[AutowiredParameter(ref: '%resultCachePath%')] + private string $cacheFilePath, + ) { - $this->cacheFilePath = $cacheFilePath; - $this->tempResultCachePath = $tempResultCachePath; } public function clear(): string @@ -29,12 +31,4 @@ public function clear(): string return $dir; } - public function clearTemporaryCaches(): void - { - $finder = new Finder(); - foreach ($finder->files()->name('*.php')->in($this->tempResultCachePath) as $tmpResultCacheFile) { - @unlink($tmpResultCacheFile->getPathname()); - } - } - } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 46b2205628..87e6b07785 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,224 +2,262 @@ namespace PHPStan\Analyser\ResultCache; -use Nette\DI\Definitions\Statement; use Nette\Neon\Neon; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; +use PHPStan\Collectors\CollectedData; use PHPStan\Command\Output; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\ExportedNode\ExportedTraitNode; use PHPStan\Dependency\ExportedNodeFetcher; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\DependencyInjection\ProjectConfigHelper; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileFinder; -use PHPStan\File\FileReader; +use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; +use PHPStan\Internal\ArrayHelper; +use PHPStan\Internal\ComposerHelper; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use Throwable; +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_merge; +use function array_unique; +use function array_values; +use function count; +use function explode; +use function get_loaded_extensions; +use function implode; +use function is_array; +use function is_dir; +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 + */ +#[GenerateFactory(interface: ResultCacheManagerFactory::class)] +final class ResultCacheManager { - private const CACHE_VERSION = 'v9-project-extensions'; - - private ExportedNodeFetcher $exportedNodeFetcher; - - private FileFinder $scanFileFinder; - - private ReflectionProvider $reflectionProvider; - - private string $cacheFilePath; - - private string $tempResultCachePath; - - /** @var string[] */ - private array $analysedPaths; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - - /** @var string[] */ - private array $stubFiles; - - private string $usedLevel; - - private ?string $cliAutoloadFile; - - /** @var string[] */ - private array $bootstrapFiles; - - /** @var string[] */ - private array $scanFiles; - - /** @var string[] */ - private array $scanDirectories; + private const CACHE_VERSION = 'v12-linesToIgnore'; /** @var array */ private array $fileHashes = []; - /** @var array */ - private array $fileReplacements = []; - /** @var array */ private array $alreadyProcessed = []; - private bool $checkDependenciesOfProjectExtensionFiles; - /** - * @param ExportedNodeFetcher $exportedNodeFetcher - * @param FileFinder $scanFileFinder - * @param ReflectionProvider $reflectionProvider - * @param string $cacheFilePath - * @param string $tempResultCachePath * @param string[] $analysedPaths + * @param string[] $analysedPathsFromConfig * @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 * @param array $fileReplacements */ public function __construct( - ExportedNodeFetcher $exportedNodeFetcher, - FileFinder $scanFileFinder, - ReflectionProvider $reflectionProvider, - string $cacheFilePath, - string $tempResultCachePath, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $stubFiles, - string $usedLevel, - ?string $cliAutoloadFile, - array $bootstrapFiles, - array $scanFiles, - array $scanDirectories, - array $fileReplacements, - bool $checkDependenciesOfProjectExtensionFiles + private Container $container, + private ExportedNodeFetcher $exportedNodeFetcher, + #[AutowiredParameter(ref: '@fileFinderScan')] + private FileFinder $scanFileFinder, + private ReflectionProvider $reflectionProvider, + private StubFilesProvider $stubFilesProvider, + private FileHelper $fileHelper, + #[AutowiredParameter(ref: '%resultCachePath%')] + private string $cacheFilePath, + #[AutowiredParameter] + private array $analysedPaths, + #[AutowiredParameter] + private array $analysedPathsFromConfig, + #[AutowiredParameter] + private array $composerAutoloaderProjectPaths, + #[AutowiredParameter] + private string $usedLevel, + #[AutowiredParameter] + private ?string $cliAutoloadFile, + #[AutowiredParameter] + private array $bootstrapFiles, + #[AutowiredParameter] + private array $scanFiles, + #[AutowiredParameter] + private array $scanDirectories, + private array $fileReplacements, + #[AutowiredParameter(ref: '%resultCacheChecksProjectExtensionFilesDependencies%')] + private bool $checkDependenciesOfProjectExtensionFiles, + #[AutowiredParameter] + private array $parametersNotInvalidatingCache, + #[AutowiredParameter(ref: '%resultCacheSkipIfOlderThanDays%')] + private int $skipResultCacheIfOlderThanDays, ) { - $this->exportedNodeFetcher = $exportedNodeFetcher; - $this->scanFileFinder = $scanFileFinder; - $this->reflectionProvider = $reflectionProvider; - $this->cacheFilePath = $cacheFilePath; - $this->tempResultCachePath = $tempResultCachePath; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->stubFiles = $stubFiles; - $this->usedLevel = $usedLevel; - $this->cliAutoloadFile = $cliAutoloadFile; - $this->bootstrapFiles = $bootstrapFiles; - $this->scanFiles = $scanFiles; - $this->scanDirectories = $scanDirectories; - $this->fileReplacements = $fileReplacements; - $this->checkDependenciesOfProjectExtensionFiles = $checkDependenciesOfProjectExtensionFiles; } /** * @param string[] $allAnalysedFiles * @param mixed[]|null $projectConfigArray - * @param bool $debug - * @return ResultCache */ - public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output, ?string $resultCacheName = null): ResultCache + public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output): ResultCache { + $startTime = microtime(true); + $currentFileHashes = []; + foreach ($allAnalysedFiles as $analysedFile) { + if (!is_file($analysedFile)) { + continue; + } + $currentFileHashes[$analysedFile] = $this->getFileHash($analysedFile); + } if ($debug) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because of debug mode.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], [], [], $currentFileHashes); } if ($onlyFiles) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because only files were passed as analysed paths.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], [], [], $currentFileHashes); } $cacheFilePath = $this->cacheFilePath; - if ($resultCacheName !== null) { - $tmpCacheFile = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - if (is_file($tmpCacheFile)) { - $cacheFilePath = $tmpCacheFile; - } - } - if (!is_file($cacheFilePath)) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because the cache file does not exist.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], [], [], $currentFileHashes); } try { $data = require $cacheFilePath; - } catch (\Throwable $e) { - if ($output->isDebug()) { + } 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), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], [], [], $currentFileHashes); } if (!is_array($data)) { @unlink($cacheFilePath); - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because the cache file is corrupted.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], [], [], $currentFileHashes); } $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); if ($this->isMetaDifferent($data['meta'], $meta)) { - if ($output->isDebug()) { - $output->writeLineFormatted('Result cache not used because the metadata do not match.'); + if ($output->isVeryVerbose()) { + $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); + $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], [], [], $currentFileHashes); } - if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * 7) { - if ($output->isDebug()) { - $output->writeLineFormatted('Result cache not used because it\'s more than 7 days since last full analysis.'); + $daysOldForSkip = $this->skipResultCacheIfOlderThanDays; + if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * $daysOldForSkip) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted(sprintf("Result cache not used because it's more than %d days since last full analysis.", $daysOldForSkip)); } - // run full analysis if the result cache is older than 7 days - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + + // run full analysis if the result cache is older than X days + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], [], [], $currentFileHashes); } - foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) { + /** + * @var string $fileHash + * @var bool $isAnalysed + */ + foreach ($data['projectExtensionFiles'] as $extensionFile => [$fileHash, $isAnalysed]) { + if (!$isAnalysed) { + continue; + } if (!is_file($extensionFile)) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], [], [], $currentFileHashes); } if ($this->getFileHash($extensionFile) === $fileHash) { continue; } - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], [], [], $currentFileHashes); } $invertedDependencies = $data['dependencies']; $deletedFiles = array_fill_keys(array_keys($invertedDependencies), true); $filesToAnalyse = []; $invertedDependenciesToReturn = []; + $invertedUsedTraitDependenciesToReturn = []; $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]; } @@ -236,7 +274,11 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $cachedFileHash = $analysedFileData['fileHash']; $dependentFiles = $analysedFileData['dependentFiles']; $invertedDependenciesToReturn[$analysedFile] = $dependentFiles; - $currentFileHash = $this->getFileHash($analysedFile); + $usedTraitDependentFiles = $analysedFileData['usedTraitDependentFiles'] ?? []; + if (count($usedTraitDependentFiles) > 0) { + $invertedUsedTraitDependenciesToReturn[$analysedFile] = $usedTraitDependentFiles; + } + $currentFileHash = $currentFileHashes[$analysedFile]; if ($cachedFileHash === $currentFileHash) { continue; @@ -248,11 +290,32 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } $cachedFileExportedNodes = $filteredExportedNodes[$analysedFile]; - if (count($dependentFiles) === 0) { + $exportedNodesChanged = $this->exportedNodesChanged($analysedFile, $cachedFileExportedNodes); + if ($exportedNodesChanged === null) { + if (count($cachedFileExportedNodes) === 0) { + continue; + } + foreach ($cachedFileExportedNodes as $exportedNode) { + if (!$exportedNode instanceof ExportedTraitNode) { + continue 2; + } + } + + // if the file changed but no exported nodes changed and the only exported nodes are traits + // reanalyse files with classes using those traits + // but not other dependent files + + foreach ($usedTraitDependentFiles as $usedTraitDependentFile) { + if (!is_file($usedTraitDependentFile)) { + continue; + } + $filesToAnalyse[] = $usedTraitDependentFile; + } continue; } - if (!$this->exportedNodesChanged($analysedFile, $cachedFileExportedNodes)) { - continue; + + if ($exportedNodesChanged) { + $newFileAppeared = true; } foreach ($dependentFiles as $dependentFile) { @@ -284,18 +347,36 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } } - return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $invertedDependenciesToReturn, $filteredExportedNodes); + $filesToAnalyse = array_unique($filesToAnalyse); + $filesToAnalyseCount = count($filesToAnalyse); + + if ($output->isVeryVerbose()) { + $elapsed = microtime(true) - $startTime; + $elapsedString = $elapsed > 5 + ? sprintf(' in %.1f seconds', $elapsed) + : ''; + + $output->writeLineFormatted(sprintf( + 'Result cache restored%s. %d %s will be reanalysed.', + $elapsedString, + $filesToAnalyseCount, + $filesToAnalyseCount === 1 ? 'file' : 'files', + )); + } + + return new ResultCache($filesToAnalyse, false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $invertedUsedTraitDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles'], $currentFileHashes); } /** * @param mixed[] $cachedMeta * @param mixed[] $currentMeta - * @return bool */ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool { $projectConfig = $currentMeta['projectConfig']; if ($projectConfig !== null) { + ksort($currentMeta['projectConfig']); + $currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']); } @@ -303,16 +384,61 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool } /** - * @param string $analysedFile - * @param array $cachedFileExportedNodes - * @return bool + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * + * @return string[] */ - private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): bool + private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): array + { + $diffs = []; + foreach ($cachedMeta as $key => $value) { + if (!array_key_exists($key, $currentMeta)) { + $diffs[] = $key; + continue; + } + + if ($value === $currentMeta[$key]) { + continue; + } + + $diffs[] = $key; + } + + if ($diffs === []) { + // when none of the keys is different, + // the order of the keys is the problem + $diffs[] = 'keyOrder'; + } + + return $diffs; + } + + /** + * @param array $cachedFileExportedNodes + * @return bool|null null means nothing changed, true means new root symbol appeared, false means nested node changed + */ + private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): ?bool { if (array_key_exists($analysedFile, $this->fileReplacements)) { $analysedFile = $this->fileReplacements[$analysedFile]; } $fileExportedNodes = $this->exportedNodeFetcher->fetchNodes($analysedFile); + + $cachedSymbols = []; + 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; } @@ -320,20 +446,14 @@ private function exportedNodesChanged(string $analysedFile, array $cachedFileExp foreach ($fileExportedNodes as $i => $fileExportedNode) { $cachedExportedNode = $cachedFileExportedNodes[$i]; if (!$cachedExportedNode->equals($fileExportedNode)) { - return true; + return false; } } - return false; + return null; } - /** - * @param AnalyserResult $analyserResult - * @param ResultCache $resultCache - * @param bool|string $save - * @return ResultCacheProcessResult - */ - public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, $save): ResultCacheProcessResult + public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, bool $save): ResultCacheProcessResult { $internalErrors = $analyserResult->getInternalErrors(); $freshErrorsByFile = []; @@ -341,35 +461,59 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $freshErrorsByFile[$error->getFilePath()][] = $error; } + $freshLocallyIgnoredErrorsByFile = []; + foreach ($analyserResult->getLocallyIgnoredErrors() as $error) { + $freshLocallyIgnoredErrorsByFile[$error->getFilePath()][] = $error; + } + + $freshCollectedDataByFile = $analyserResult->getCollectedData(); + $meta = $resultCache->getMeta(); - $doSave = function (array $errorsByFile, ?array $dependencies, array $exportedNodes, ?string $resultCacheName) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { + $projectConfigArray = $meta['projectConfig']; + if ($projectConfigArray !== null) { + $meta['projectConfig'] = Neon::encode($projectConfigArray); + } + $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, ?array $usedTraitDependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { if ($onlyFiles) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.'); } return false; } if ($dependencies === null) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because of error in dependencies.'); } return false; } + if ($usedTraitDependencies === null) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because of error in used trait dependencies.'); + } + return false; + } if (count($internalErrors) > 0) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because of internal errors.'); } return false; } + if (count($this->fileReplacements) > 0) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because of --tmp-file and --instead-of CLI options passed (editor mode).'); + } + return false; + } + foreach ($errorsByFile as $errors) { foreach ($errors as $error) { if (!$error->hasNonIgnorableException()) { continue; } - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache was not saved because of non-ignorable exception: %s', $error->getMessage())); } @@ -377,9 +521,9 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $resultCacheName, $errorsByFile, $dependencies, $exportedNodes, $meta); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles, $resultCache->getCurrentFileHashes(), $meta); - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache is saved.'); } @@ -389,9 +533,13 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache if ($resultCache->isFullAnalysis()) { $saved = false; if ($save !== false) { - $saved = $doSave($freshErrorsByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), is_string($save) ? $save : null); + $projectExtensionFiles = []; + if ($analyserResult->getDependencies() !== null) { + $projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies()); + } + $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); } } @@ -400,12 +548,37 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } $errorsByFile = $this->mergeErrors($resultCache, $freshErrorsByFile); - $dependencies = $this->mergeDependencies($resultCache, $analyserResult->getDependencies()); + $locallyIgnoredErrorsByFile = $this->mergeLocallyIgnoredErrors($resultCache, $freshLocallyIgnoredErrorsByFile); + $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); + $dependencies = $this->mergeDependencies($resultCache->getDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getDependencies()); + $usedTraitDependencies = $this->mergeDependencies($resultCache->getUsedTraitDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getUsedTraitDependencies()); $exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes()); + $linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore()); + $unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores()); $saved = false; if ($save !== false) { - $saved = $doSave($errorsByFile, $dependencies, $exportedNodes, is_string($save) ? $save : null); + $projectExtensionFiles = []; + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + // keep the same file hashes from the old run + // so that the message "When you edit them and re-run PHPStan, the result cache will get stale." + // keeps being shown on subsequent runs + $projectExtensionFiles[$file] = [$hash, false, $className]; + } + if ($dependencies !== null) { + foreach ($this->getProjectExtensionFiles($projectConfigArray, $dependencies) as $file => [$hash, $isAnalysed, $className]) { + if (!$isAnalysed) { + continue; + } + + $projectExtensionFiles[$file] = [$hash, true, $className]; + } + } + $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -415,24 +588,42 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } + $flatLocallyIgnoredErrors = []; + foreach ($locallyIgnoredErrorsByFile as $fileErrors) { + foreach ($fileErrors as $fileError) { + $flatLocallyIgnoredErrors[] = $fileError; + } + } + return new ResultCacheProcessResult(new AnalyserResult( $flatErrors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $flatLocallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, $internalErrors, + $collectedDataByFile, $dependencies, + $usedTraitDependencies, $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit() + $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 { $errorsByFile = $resultCache->getErrors(); foreach ($resultCache->getFilesToAnalyse() as $file) { + if (array_key_exists($file, $this->fileReplacements)) { + unset($errorsByFile[$file]); + $file = $this->fileReplacements[$file]; + } if (!array_key_exists($file, $freshErrorsByFile)) { unset($errorsByFile[$file]); continue; @@ -444,18 +635,62 @@ 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, $this->fileReplacements)) { + unset($errorsByFile[$file]); + $file = $this->fileReplacements[$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, $this->fileReplacements)) { + unset($collectedDataByFile[$file]); + $file = $this->fileReplacements[$file]; + } + if (!array_key_exists($file, $freshCollectedDataByFile)) { + unset($collectedDataByFile[$file]); + continue; + } + $collectedDataByFile[$file] = $freshCollectedDataByFile[$file]; + } + + return $collectedDataByFile; + } + + /** + * @param array> $resultCacheDependencies + * @param string[] $filesToAnalyse * @param array>|null $freshDependencies * @return array>|null */ - private function mergeDependencies(ResultCache $resultCache, ?array $freshDependencies): ?array + private function mergeDependencies(array $resultCacheDependencies, array $filesToAnalyse, ?array $freshDependencies): ?array { if ($freshDependencies === null) { return null; } $cachedDependencies = []; - $resultCacheDependencies = $resultCache->getDependencies(); $filesNoOneIsDependingOn = array_fill_keys(array_keys($resultCacheDependencies), true); foreach ($resultCacheDependencies as $file => $filesDependingOnFile) { foreach ($filesDependingOnFile as $fileDependingOnFile) { @@ -466,14 +701,18 @@ 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] = []; } $newDependencies = $cachedDependencies; - foreach ($resultCache->getFilesToAnalyse() as $file) { + foreach ($filesToAnalyse as $file) { + if (array_key_exists($file, $this->fileReplacements)) { + unset($newDependencies[$file]); + $file = $this->fileReplacements[$file]; + } if (!array_key_exists($file, $freshDependencies)) { unset($newDependencies[$file]); continue; @@ -486,14 +725,17 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend } /** - * @param ResultCache $resultCache - * @param array> $freshExportedNodes - * @return array> + * @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, $this->fileReplacements)) { + unset($newExportedNodes[$file]); + $file = $this->fileReplacements[$file]; + } if (!array_key_exists($file, $freshExportedNodes)) { unset($newExportedNodes[$file]); continue; @@ -506,20 +748,77 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport } /** - * @param int $lastFullAnalysisTime - * @param string|null $resultCacheName - * @param array> $errors + * @param array $freshLinesToIgnore + * @return array + */ + private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesToIgnore): array + { + $newLinesToIgnore = $resultCache->getLinesToIgnore(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (array_key_exists($file, $this->fileReplacements)) { + unset($newLinesToIgnore[$file]); + $file = $this->fileReplacements[$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, $this->fileReplacements)) { + unset($newUnmatchedLineIgnores[$file]); + $file = $this->fileReplacements[$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> $usedTraitDependencies + * @param array> $exportedNodes + * @param array $projectExtensionFiles + * @param array $currentFileHashes * @param mixed[] $meta */ private function save( int $lastFullAnalysisTime, - ?string $resultCacheName, array $errors, + array $locallyIgnoredErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + array $collectedData, array $dependencies, + array $usedTraitDependencies, array $exportedNodes, - array $meta + array $projectExtensionFiles, + array $currentFileHashes, + array $meta, ): void { $invertedDependencies = []; @@ -528,7 +827,7 @@ private function save( foreach ($fileDependencies as $fileDep) { if (!array_key_exists($fileDep, $invertedDependencies)) { $invertedDependencies[$fileDep] = [ - 'fileHash' => $this->getFileHash($fileDep), + 'fileHash' => $currentFileHashes[$fileDep] ?? $this->getFileHash($fileDep), 'dependentFiles' => [], ]; unset($filesNoOneIsDependingOn[$fileDep]); @@ -537,9 +836,23 @@ private function save( } } + foreach ($usedTraitDependencies as $file => $fileUsedTraitDependencies) { + foreach ($fileUsedTraitDependencies as $usedTraitFileDep) { + if (!array_key_exists($usedTraitFileDep, $invertedDependencies)) { + $invertedDependencies[$usedTraitFileDep] = [ + 'fileHash' => $currentFileHashes[$usedTraitFileDep] ?? $this->getFileHash($usedTraitFileDep), + 'dependentFiles' => [], + 'usedTraitDependentFiles' => [], + ]; + unset($filesNoOneIsDependingOn[$usedTraitFileDep]); + } + $invertedDependencies[$usedTraitFileDep]['usedTraitDependentFiles'][] = $file; + } + } + foreach (array_keys($filesNoOneIsDependingOn) as $file) { if (array_key_exists($file, $invertedDependencies)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_file($file)) { @@ -547,135 +860,122 @@ private function save( } $invertedDependencies[$file] = [ - 'fileHash' => $this->getFileHash($file), + 'fileHash' => $currentFileHashes[$file] ?? $this->getFileHash($file), 'dependentFiles' => [], ]; } ksort($errors); + ksort($locallyIgnoredErrors); + ksort($linesToIgnore); + ksort($unmatchedLineIgnores); + ksort($collectedData); ksort($invertedDependencies); + foreach ($collectedData as & $collectedDataPerFile) { + ksort($collectedDataPerFile); + } + foreach ($invertedDependencies as $file => $fileData) { $dependentFiles = $fileData['dependentFiles']; sort($dependentFiles); $invertedDependencies[$file]['dependentFiles'] = $dependentFiles; - } - $template = <<<'php' - %s, - 'meta' => %s, - 'projectExtensionFiles' => %s, - 'errorsCallback' => static function (): array { return %s; }, - 'dependencies' => %s, - 'exportedNodesCallback' => static function (): array { return %s; }, -]; -php; + sort($usedTraitDependentFiles); + $invertedDependencies[$file]['usedTraitDependentFiles'] = $usedTraitDependentFiles; + } ksort($exportedNodes); $file = $this->cacheFilePath; - if ($resultCacheName !== null) { - $file = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - } - - $projectConfigArray = $meta['projectConfig']; - if ($projectConfigArray !== null) { - $meta['projectConfig'] = Neon::encode($projectConfigArray); - } FileWriter::write( $file, - sprintf( - $template, - var_export($lastFullAnalysisTime, true), - var_export($meta, true), - var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true), - var_export($errors, true), - var_export($invertedDependencies, true), - var_export($exportedNodes, true) - ) + " " . var_export($lastFullAnalysisTime, true) . ", + 'meta' => " . var_export($meta, true) . ", + 'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ", + 'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; }, + 'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; }, + 'linesToIgnore' => " . var_export($linesToIgnore, true) . ", + 'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ", + 'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; }, + 'dependencies' => " . var_export($invertedDependencies, true) . ", + 'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; }, +]; +', ); } /** * @param mixed[]|null $projectConfig * @param array $dependencies - * @return array + * @return array */ private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array { $this->alreadyProcessed = []; $projectExtensionFiles = []; if ($projectConfig !== null) { - $services = array_merge( - $projectConfig['services'] ?? [], - $projectConfig['rules'] ?? [] - ); - foreach ($services as $service) { - $classes = $this->getClassesFromConfigDefinition($service); - if (is_array($service)) { - foreach (['class', 'factory', 'implement'] as $key) { - if (!isset($service[$key])) { - continue; - } + $vendorDirs = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloaderProjectPath) { + $composer = ComposerHelper::getComposerConfig($autoloaderProjectPath); + if ($composer === null) { + continue; + } + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($autoloaderProjectPath, $composer); + $vendorDirs[] = $this->fileHelper->normalizePath($vendorDirectory); + } - $classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key])); - } + $classes = ProjectConfigHelper::getServiceClassNames($projectConfig); + foreach ($classes as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + continue; } - foreach (array_unique($classes) as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - continue; - } + $classReflection = $this->reflectionProvider->getClass($class); + $fileName = $classReflection->getFileName(); + if ($fileName === null) { + continue; + } - $classReflection = $this->reflectionProvider->getClass($class); - $fileName = $classReflection->getFileName(); - if ($fileName === null) { - continue; - } + if (str_starts_with($fileName, 'phar://')) { + continue; + } - $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); - foreach ($allServiceFiles as $serviceFile) { - if (array_key_exists($serviceFile, $projectExtensionFiles)) { - continue; + $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); + if (count($allServiceFiles) === 0) { + $normalizedFileName = $this->fileHelper->normalizePath($fileName); + foreach ($vendorDirs as $vendorDir) { + if (str_starts_with($normalizedFileName, $vendorDir)) { + continue 2; } - - $projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile); } + $projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class]; + continue; } - } - } - return $projectExtensionFiles; - } - - /** - * @param mixed $definition - * @return string[] - */ - private function getClassesFromConfigDefinition($definition): array - { - if (is_string($definition)) { - return [$definition]; - } + foreach ($allServiceFiles as $serviceFile) { + if (array_key_exists($serviceFile, $projectExtensionFiles)) { + continue; + } - if ($definition instanceof Statement) { - $entity = $definition->entity; - if (is_string($entity)) { - return [$entity]; - } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { - return [$entity[0]]; + $projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class]; + } } } - return []; + return $projectExtensionFiles; } /** - * @param string $fileName * @param array> $dependencies * @return array */ @@ -711,25 +1011,22 @@ private function getAllDependencies(string $fileName, array $dependencies): arra */ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): array { - $extensions = array_values(array_filter(get_loaded_extensions(), static function (string $extension): bool { - return $extension !== 'xdebug'; - })); + $extensions = array_values(array_filter(get_loaded_extensions(), static fn (string $extension): bool => $extension !== 'xdebug')); sort($extensions); if ($projectConfigArray !== null) { - unset($projectConfigArray['parameters']['ignoreErrors']); - unset($projectConfigArray['parameters']['tipsOfTheDay']); - unset($projectConfigArray['parameters']['parallel']); - unset($projectConfigArray['parameters']['internalErrorsCountLimit']); - unset($projectConfigArray['parameters']['cache']); - unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']); - unset($projectConfigArray['parameters']['memoryLimitFile']); - unset($projectConfigArray['parametersSchema']); + foreach ($this->parametersNotInvalidatingCache as $parameterPath) { + $pathAsArray = is_array($parameterPath) ? $parameterPath : explode('.', $parameterPath); + ArrayHelper::unsetKeyAtPath($projectConfigArray, $pathAsArray); + } + + ksort($projectConfigArray); } return [ 'cacheVersion' => self::CACHE_VERSION, - 'phpstanVersion' => $this->getPhpStanVersion(), + 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'metaExtensions' => $this->getMetaFromPhpStanExtensions(), 'phpVersion' => PHP_VERSION_ID, 'projectConfig' => $projectConfigArray, 'analysedPaths' => $this->analysedPaths, @@ -752,10 +1049,10 @@ private function getFileHash(string $path): string return $this->fileHashes[$path]; } - $contents = FileReader::read($path); - $contents = str_replace("\r\n", "\n", $contents); - - $hash = sha1($contents); + $hash = sha1_file($path); + if ($hash === false) { + throw new CouldNotReadFileException($path); + } $this->fileHashes[$path] = $hash; return $hash; @@ -768,11 +1065,23 @@ private function getFileHash(string $path): string private function getScannedFiles(array $allAnalysedFiles): array { $scannedFiles = $this->scanFiles; - foreach ($this->scanFileFinder->findFiles($this->scanDirectories)->getFiles() as $file) { - $scannedFiles[] = $file; + $analysedDirectories = []; + foreach (array_merge($this->analysedPaths, $this->analysedPathsFromConfig) as $analysedPath) { + if (is_file($analysedPath)) { + continue; + } + + if (!is_dir($analysedPath)) { + continue; + } + + $analysedDirectories[] = $analysedPath; } - $scannedFiles = array_unique($scannedFiles); + $directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories)); + foreach ($this->scanFileFinder->findFiles($directories)->getFiles() as $file) { + $scannedFiles[] = $file; + } $hashes = []; foreach (array_diff($scannedFiles, $allAnalysedFiles) as $file) { @@ -803,15 +1112,6 @@ private function getExecutedFileHashes(): array return $hashes; } - private function getPhpStanVersion(): string - { - try { - return \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - return 'Version unknown'; - } - } - /** * @return array */ @@ -831,18 +1131,28 @@ private function getComposerLocks(): array } /** - * @return array + * @return array> */ private function getComposerInstalled(): array { $data = []; foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { - $filePath = $autoloadPath . '/vendor/composer/installed.php'; + $composer = ComposerHelper::getComposerConfig($autoloadPath); + + if ($composer === null) { + continue; + } + + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; if (!is_file($filePath)) { continue; } $installed = require $filePath; + if (!is_array($installed)) { + throw new ShouldNotHappenException(); + } + $rootName = $installed['root']['name']; unset($installed['root']); unset($installed['versions'][$rootName]); @@ -859,7 +1169,7 @@ private function getComposerInstalled(): array private function getStubFiles(): array { $stubFiles = []; - foreach ($this->stubFiles as $stubFile) { + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { $stubFiles[$stubFile] = $this->getFileHash($stubFile); } @@ -868,4 +1178,29 @@ private function getStubFiles(): array return $stubFiles; } + /** + * @return array + * @throws ShouldNotHappenException + */ + private function getMetaFromPhpStanExtensions(): array + { + $meta = []; + + /** @var ResultCacheMetaExtension $extension */ + foreach ($this->container->getServicesByTag(ResultCacheMetaExtension::EXTENSION_TAG) as $extension) { + if (array_key_exists($extension->getKey(), $meta)) { + throw new ShouldNotHappenException(sprintf( + 'Duplicate ResultCacheMetaExtension with key "%s" found.', + $extension->getKey(), + )); + } + + $meta[$extension->getKey()] = $extension->getHash(); + } + + ksort($meta); + + return $meta; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheManagerFactory.php b/src/Analyser/ResultCache/ResultCacheManagerFactory.php index c2bdadefaa..333bc6136e 100644 --- a/src/Analyser/ResultCache/ResultCacheManagerFactory.php +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -7,7 +7,6 @@ interface ResultCacheManagerFactory /** * @param array $fileReplacements - * @return ResultCacheManager */ public function create(array $fileReplacements): ResultCacheManager; diff --git a/src/Analyser/ResultCache/ResultCacheMetaExtension.php b/src/Analyser/ResultCache/ResultCacheMetaExtension.php new file mode 100644 index 0000000000..11aac2512f --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheMetaExtension.php @@ -0,0 +1,39 @@ +analyserResult = $analyserResult; - $this->saved = $saved; } public function getAnalyserResult(): AnalyserResult diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php new file mode 100644 index 0000000000..96337e87f5 --- /dev/null +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -0,0 +1,96 @@ + + */ + 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() + ) { + $foundPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($expr->left, $scope); + foreach ($foundPropertyReflections as $foundPropertyReflection) { + if ($foundPropertyReflection->isNative() && !$foundPropertyReflection->hasNativeType()) { + return new TypeResult(new BooleanType(), []); + } + } + } + + if ( + ( + $expr->right instanceof Node\Expr\PropertyFetch + || $expr->right instanceof Node\Expr\StaticPropertyFetch + ) + && $leftType->isNull()->yes() + ) { + $foundPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($expr->right, $scope); + foreach ($foundPropertyReflections as $foundPropertyReflection) { + if ($foundPropertyReflection->isNative() && !$foundPropertyReflection->hasNativeType()) { + 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..ebbc7679e9 --- /dev/null +++ b/src/Analyser/RuleErrorTransformer.php @@ -0,0 +1,171 @@ +differ = new Differ(new UnifiedDiffOutputBuilder('', addLineNumbers: true)); + } + + /** + * @param Node\Stmt[] $fileNodes + */ + public function transform( + RuleError $ruleError, + Scope $scope, + array $fileNodes, + Node $node, + ): Error + { + $line = $node->getStartLine(); + $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; + } + + $fixedErrorDiff = null; + if ($ruleError instanceof FixableNodeRuleError) { + if ($ruleError->getOriginalNode() instanceof VirtualNode) { + throw new ShouldNotHappenException('Cannot fix virtual node'); + } + $fixingFile = $filePath; + if ($traitFilePath !== null) { + $fixingFile = $traitFilePath; + } + + $oldCode = FileReader::read($fixingFile); + + $this->parser->parse($oldCode); + $hash = sha1($oldCode); + $oldTokens = $this->parser->getTokens(); + + $indentTraverser = new NodeTraverser(); + $indentDetector = new PhpPrinterIndentationDetectorVisitor(new TokenStream($oldTokens, PhpPrinter::TAB_WIDTH)); + $indentTraverser->addVisitor($indentDetector); + $indentTraverser->traverse($fileNodes); + + $cloningTraverser = new NodeTraverser(); + $cloningTraverser->addVisitor(new UnwrapVirtualNodesVisitor()); + $cloningTraverser->addVisitor(new CloningVisitor()); + + /** @var Stmt[] $newStmts */ + $newStmts = $cloningTraverser->traverse($fileNodes); + + $traverser = new NodeTraverser(); + $visitor = new ReplacingNodeVisitor($ruleError->getOriginalNode(), $ruleError->getNewNodeCallable()); + $traverser->addVisitor($visitor); + + /** @var Stmt[] $newStmts */ + $newStmts = $traverser->traverse($newStmts); + + if ($visitor->isFound()) { + if (str_contains($indentDetector->indentCharacter, "\t")) { + $indent = "\t"; + } else { + $indent = str_repeat($indentDetector->indentCharacter, $indentDetector->indentSize); + } + $printer = new PhpPrinter(['indent' => $indent]); + $newCode = $printer->printFormatPreserving($newStmts, $fileNodes, $oldTokens); + + if ($oldCode !== $newCode) { + $fixedErrorDiff = new FixedErrorDiff($hash, $this->differ->diff($oldCode, $newCode)); + } + } + } + + return new Error( + $ruleError->getMessage(), + $fileName, + $line, + $canBeIgnored, + $filePath, + $traitFilePath, + $tip, + $node->getStartLine(), + get_class($node), + $identifier, + $metadata, + $fixedErrorDiff, + ); + } + +} diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index bcd205a9a7..d7397399f6 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -2,41 +2,59 @@ 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\PropertyReflection; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; /** @api */ -interface Scope extends ClassMemberAccessAnswerer +interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer { + public const SUPERGLOBAL_VARIABLES = [ + 'GLOBALS', + '_SERVER', + '_GET', + '_POST', + '_FILES', + '_COOKIE', + '_SESSION', + '_REQUEST', + '_ENV', + ]; + public function getFile(): string; public function getFileDescription(): string; public function isDeclareStrictTypes(): bool; + /** + * @phpstan-assert-if-true !null $this->getTraitReflection() + */ public function isInTrait(): bool; public function getTraitReflection(): ?ClassReflection; - /** - * @return \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; @@ -50,31 +68,45 @@ public function canAnyVariableExist(): bool; */ public function getDefinedVariables(): array; + /** + * @return array + */ + public function getMaybeDefinedVariables(): array; + public function hasConstant(Name $name): bool; - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection; + /** @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead */ + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection; + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection; + public function getConstantExplicitTypeFromConfig(string $constantName, Type $constantType): Type; + + 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; @@ -85,28 +117,37 @@ public function resolveTypeByName(Name $name): TypeWithClassName; */ public function getTypeFromValue($value): Type; - public function isSpecified(Expr $node): bool; + public function hasExpressionType(Expr $node): TrinaryLogic; public function isInClassExists(string $className): bool; + 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\ComplexType|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 isUndefinedExpressionAllowed(Expr $expr): bool; + public function filterByTruthyValue(Expr $expr): self; public function filterByFalseyValue(Expr $expr): self; public function isInFirstLevelStatement(): bool; + public function getPhpVersion(): PhpVersions; + } diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index ea03d664f9..dfa0c1f17b 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -3,25 +3,17 @@ namespace PHPStan\Analyser; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; -class ScopeContext +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 */ @@ -38,10 +30,10 @@ public function beginFile(): self public function enterClass(ClassReflection $classReflection): self { if ($this->classReflection !== null && !$classReflection->isAnonymous()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($classReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $classReflection, null); } @@ -49,10 +41,10 @@ public function enterClass(ClassReflection $classReflection): self public function enterTrait(ClassReflection $traitReflection): self { if ($this->classReflection === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$traitReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $this->classReflection, $traitReflection); diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index b0fd874e7d..be9ca1982d 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,52 +2,22 @@ namespace PHPStan\Analyser; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Type\Type; +use PHPStan\DependencyInjection\AutowiredService; -interface ScopeFactory +/** + * @api + */ +#[AutowiredService] +final class ScopeFactory { - /** - * @api - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes - * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null - ): MutatingScope; + public function __construct(private InternalScopeFactory $internalScopeFactory) + { + } + + public function create(ScopeContext $context): MutatingScope + { + return $this->internalScopeFactory->create($context); + } } diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index 73e4ff2559..fd9ddda81d 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -6,38 +6,79 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SpecifiedTypes +final class SpecifiedTypes { - /** @var array */ - private array $sureTypes; - - /** @var array */ - private array $sureNotTypes; - - private bool $overwrite; + private bool $overwrite = false; /** @var array */ - private array $newConditionalExpressionHolders; + private array $newConditionalExpressionHolders = []; + + private ?Expr $rootExpr = null; /** * @api * @param array $sureTypes * @param array $sureNotTypes - * @param bool $overwrite - * @param array $newConditionalExpressionHolders */ public function __construct( - array $sureTypes = [], - array $sureNotTypes = [], - bool $overwrite = false, - array $newConditionalExpressionHolders = [] + private array $sureTypes = [], + private array $sureNotTypes = [], ) { - $this->sureTypes = $sureTypes; - $this->sureNotTypes = $sureNotTypes; - $this->overwrite = $overwrite; - $this->newConditionalExpressionHolders = $newConditionalExpressionHolders; + } + + /** + * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type. + * And $sureNotTypes are used to remove type from the pre-existing type. + * + * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType. + * Because it's not possible to narrow non-empty-string to an empty string. + * + * In rare cases, a type-specifying extension might want to overwrite the pre-existing types + * without taking the pre-existing types into consideration. + * + * In that case it should also call setAlwaysOverwriteTypes() on + * the returned object. + * + * ! Only do this if you're certain. Otherwise, this is a source of common bugs. ! + * + * @api + */ + public function setAlwaysOverwriteTypes(): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = true; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + + /** + * @api + */ + public function setRootExpr(?Expr $rootExpr): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $rootExpr; + + return $self; + } + + /** + * @param array $newConditionalExpressionHolders + */ + public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; } /** @@ -71,11 +112,17 @@ public function getNewConditionalExpressionHolders(): array return $this->newConditionalExpressionHolders; } + public function getRootExpr(): ?Expr + { + return $this->rootExpr; + } + /** @api */ public function intersectWith(SpecifiedTypes $other): self { $sureTypeUnion = []; $sureNotTypeUnion = []; + $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr); foreach ($this->sureTypes as $exprString => [$exprNode, $type]) { if (!isset($other->sureTypes[$exprString])) { @@ -99,7 +146,12 @@ public function intersectWith(SpecifiedTypes $other): self ]; } - return new self($sureTypeUnion, $sureNotTypeUnion); + $result = new self($sureTypeUnion, $sureNotTypeUnion); + if ($this->overwrite && $other->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($rootExpr); } /** @api */ @@ -107,6 +159,7 @@ public function unionWith(SpecifiedTypes $other): self { $sureTypeUnion = $this->sureTypes + $other->sureTypes; $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes; + $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr); foreach ($this->sureTypes as $exprString => [$exprNode, $type]) { if (!isset($other->sureTypes[$exprString])) { @@ -130,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 9636198738..dad528dc18 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -2,44 +2,31 @@ namespace PHPStan\Analyser; -use PhpParser\Node\Scalar\LNumber; +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; - - /** @var ThrowPoint[] */ - private array $throwPoints; - /** - * @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, - array $throwPoints + 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; - $this->throwPoints = $throwPoints; } public function getScope(): MutatingScope @@ -70,15 +57,15 @@ public function filterOutLoopExitPoints(): self } $num = $statement->num; - if (!$num instanceof LNumber) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + if (!$num instanceof Int_) { + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } return $this; @@ -94,7 +81,7 @@ public function getExitPoints(): array /** * @param class-string|class-string $stmtClass - * @return StatementExitPoint[] + * @return list */ public function getExitPointsByType(string $stmtClass): array { @@ -111,7 +98,7 @@ public function getExitPointsByType(string $stmtClass): array continue; } - if (!$value instanceof LNumber) { + if (!$value instanceof Int_) { $exitPoints[] = $exitPoint; continue; } @@ -128,7 +115,7 @@ public function getExitPointsByType(string $stmtClass): array } /** - * @return StatementExitPoint[] + * @return list */ public function getExitPointsForOuterLoop(): array { @@ -142,7 +129,7 @@ public function getExitPointsForOuterLoop(): array if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -152,7 +139,7 @@ public function getExitPointsForOuterLoop(): array $newNode = null; if ($value > 2) { - $newNode = new LNumber($value - 1); + $newNode = new Int_($value - 1); } if ($statement instanceof Stmt\Continue_) { $newStatement = new Stmt\Continue_($newNode); @@ -174,4 +161,33 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * Top-level StatementResult represents the state of the code + * at the end of control flow statements like If_ or TryCatch. + * + * It shows how Scope etc. looks like after If_ no matter + * which code branch was executed. + * + * For If_, "end statements" contain the state of the code + * at the end of each branch - if, elseifs, else, including the last + * statement node in each branch. + * + * For nested ifs, end statements try to contain the last non-control flow + * statement like Return_ or Throw_, instead of If_, TryCatch, or Foreach_. + * + * @return EndStatementResult[] + */ + public function getEndStatements(): array + { + return $this->endStatements; + } + } diff --git a/src/Analyser/ThrowPoint.php b/src/Analyser/ThrowPoint.php index 4a63f3dcce..873c11e425 100644 --- a/src/Analyser/ThrowPoint.php +++ b/src/Analyser/ThrowPoint.php @@ -6,49 +6,29 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; -class ThrowPoint +/** + * @api + */ +final class ThrowPoint { - private MutatingScope $scope; - - private Type $type; - - /** @var Node\Expr|Node\Stmt */ - private Node $node; - - private bool $explicit; - - private bool $canContainAnyThrowable; - /** - * @param MutatingScope $scope - * @param Type $type * @param Node\Expr|Node\Stmt $node - * @param bool $explicit - * @param bool $canContainAnyThrowable */ private function __construct( - MutatingScope $scope, - Type $type, - Node $node, - bool $explicit, - bool $canContainAnyThrowable + private MutatingScope $scope, + private Type $type, + private Node $node, + private bool $explicit, + private bool $canContainAnyThrowable, ) { - $this->scope = $scope; - $this->type = $type; - $this->node = $node; - $this->explicit = $explicit; - $this->canContainAnyThrowable = $canContainAnyThrowable; } /** - * @param MutatingScope $scope - * @param Type $type * @param Node\Expr|Node\Stmt $node - * @param bool $canContainAnyThrowable - * @return self */ public static function createExplicit(MutatingScope $scope, Type $type, Node $node, bool $canContainAnyThrowable): self { @@ -56,13 +36,11 @@ public static function createExplicit(MutatingScope $scope, Type $type, Node $no } /** - * @param MutatingScope $scope * @param Node\Expr|Node\Stmt $node - * @return self */ public static function createImplicit(MutatingScope $scope, Node $node): self { - return new self($scope, new ObjectType(\Throwable::class), $node, false, true); + return new self($scope, new ObjectType(Throwable::class), $node, false, true); } public function getScope(): MutatingScope diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 5a6453374b..ee8069e738 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,88 +10,109 @@ 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\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\TypeExpr; +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\ConstantType; +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; +use function array_key_exists; +use function array_map; +use function array_merge; use function array_reverse; - -class TypeSpecifier +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; + +#[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')] +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; @@ -98,19 +120,19 @@ public function __construct( $extension->setTypeSpecifier($this); } - - $this->functionTypeSpecifyingExtensions = $functionTypeSpecifyingExtensions; - $this->methodTypeSpecifyingExtensions = $methodTypeSpecifyingExtensions; - $this->staticMethodTypeSpecifyingExtensions = $staticMethodTypeSpecifyingExtensions; } /** @api */ public function specifyTypesInCondition( Scope $scope, Expr $expr, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + if ($expr instanceof Instanceof_) { $exprNode = $expr->expr; if ($expr->class instanceof Name) { @@ -132,18 +154,21 @@ public function specifyTypesInCondition( } else { $type = new ObjectType($className); } - return $this->create($exprNode, $type, $context, false, $scope); + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } $classType = $scope->getType($expr->class); - $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse): Type { + $uncertainty = false; + $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use (&$uncertainty): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { + $uncertainty = true; return $type; } if ($type instanceof GenericClassStringType) { + $uncertainty = true; return $type->getGenericType(); } if ($type instanceof ConstantStringType) { @@ -156,292 +181,72 @@ public function specifyTypesInCondition( if ($context->true()) { $type = TypeCombinator::intersect( $type, - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ); - return $this->create($exprNode, $type, $context, false, $scope); - } elseif ($context->false()) { + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); + } elseif ($context->false() && !$uncertainty) { $exprType = $scope->getType($expr->expr); if (!$type->isSuperTypeOf($exprType)->yes()) { - return $this->create($exprNode, $type, $context, false, $scope); + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } } } if ($context->true()) { - return $this->create($exprNode, new ObjectWithoutClassType(), $context, false, $scope); + return $this->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode); } } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var \PHPStan\Type\ConstantScalarType $constantType */ - $constantType = $expressions[1]; - if ($constantType->getValue() === false) { - $types = $this->create($exprNode, $constantType, $context, false, $scope); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate() - )); - } - - if ($constantType->getValue() === true) { - $types = $this->create($exprNode, $constantType, $context, false, $scope); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate() - )); - } - - if ($constantType->getValue() === null) { - return $this->create($exprNode, $constantType, $context, false, $scope); - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isArray()->yes()) { - return $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope); - } - } - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'strlen' - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType instanceof StringType) { - return $this->create($exprNode->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $newContext, false, $scope); - } - } - } - } - - if ($context->true()) { - $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); - $leftTypes = $this->create($expr->left, $type, $context, false, $scope); - $rightTypes = $this->create($expr->right, $type, $context, false, $scope); - return $leftTypes->unionWith($rightTypes); - - } elseif ($context->false()) { - $identicalType = $scope->getType($expr); - if ($identicalType instanceof ConstantBooleanType) { - $never = new NeverType(); - $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; - $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope); - $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope); - return $leftTypes->unionWith($rightTypes); - } - - $exprLeftType = $scope->getType($expr->left); - $exprRightType = $scope->getType($expr->right); - - $types = null; - - if ( - $exprLeftType instanceof ConstantType - && !$expr->right instanceof Node\Scalar - ) { - $types = $this->create( - $expr->right, - $exprLeftType, - $context, - false, - $scope - ); - } - if ( - $exprRightType instanceof ConstantType - && !$expr->left instanceof Node\Scalar - ) { - $leftType = $this->create( - $expr->left, - $exprRightType, - $context, - false, - $scope - ); - if ($types !== null) { - $types = $types->unionWith($leftType); - } else { - $types = $leftType; - } - } - - if ($types !== null) { - return $types; - } - } + return $this->resolveIdentical($expr, $scope, $context); } elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) { return $this->specifyTypesInCondition( $scope, new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Identical($expr->left, $expr->right)), - $context - ); + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Bool_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\Equal($expr->expr, new ConstFetch(new Name\FullyQualified('true'))), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\String_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\String_('')), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Int_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\LNumber(0)), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Double) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\DNumber(0.0)), + $context, + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var \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 ($constantType->getValue() === true) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate() - ); - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isArray()->yes()) { - return $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope); - } - } - } - } - - $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 - ); - } - - $rightBooleanType = $rightType->toBoolean(); - if ($rightBooleanType instanceof ConstantBooleanType && $leftType instanceof BooleanType) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\Identical( - $expr->left, - new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')) - ), - $context - ); - } - - if ( - $context->falsey() - && $rightType->isArray()->yes() - && $leftType instanceof ConstantArrayType && $leftType->isEmpty() - ) { - return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope); - } - - if ( - $context->falsey() - && $leftType->isArray()->yes() - && $rightType instanceof ConstantArrayType && $rightType->isEmpty() - ) { - return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope); - } - - if ( - $expr->left instanceof FuncCall - && $expr->left->name instanceof Name - && strtolower($expr->left->name->toString()) === 'get_class' - && isset($expr->left->getArgs()[0]) - && $rightType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->left->getArgs()[0]->value, - new Name($rightType->getValue()) - ), - $context - ); - } - - if ( - $expr->right instanceof FuncCall - && $expr->right->name instanceof Name - && strtolower($expr->right->name->toString()) === 'get_class' - && isset($expr->right->getArgs()[0]) - && $leftType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->right->getArgs()[0]->value, - new Name($leftType->getValue()) - ), - $context - ); - } + 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 - ); + $context, + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { - $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); if ( $expr->left instanceof FuncCall - && count($expr->left->getArgs()) === 1 + && count($expr->left->getArgs()) >= 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true) + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -451,46 +256,122 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition( $scope, new Node\Expr\BooleanNot($inverseOperator), - $context - ); + $context, + )->setRootExpr($expr); } - $result = new SpecifiedTypes(); + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + $leftType = $scope->getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); if ( !$context->null() && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) === 1 + && count($expr->right->getArgs()) >= 1 && $expr->right->name instanceof Name && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && $leftType->isInteger()->yes() ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + $sizeType = $leftType->shift($offset); + } else { + $sizeType = $leftType; + } + + $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); + } + if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); + $countables[] = $innerType; + } + + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $countables[] = $innerType; + } + } + + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); + + return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + } + } + if ($argType->isArray()->yes()) { - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope)); + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + } + + $result = $result->unionWith( + $this->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), + ); } } } + if ( + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) >= 3 + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && ( + IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() + || ($expr instanceof Expr\BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) + ) + ) { + // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match + $newExpr = new Expr\BinaryOp\Identical($expr->right, new Node\Scalar\Int_(1)); + + return $this->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr); + } + if ( !$context->null() && $expr->right instanceof FuncCall && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name - && strtolower((string) $expr->right->name) === 'strlen' - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && $leftType->isInteger()->yes() ) { if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType instanceof StringType) { - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $context, false, $scope)); + if ($argType->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); + + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); } } } @@ -498,43 +379,50 @@ public function specifyTypesInCondition( if ($leftType instanceof ConstantIntegerType) { if ($expr->right instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), - $context + $context, )); } elseif ($expr->right instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), - $context + $context, )); } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), - $context + $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 + $context, )); } elseif ($expr->left instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), - $context + $context, )); } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), - $context + $context, )); } } @@ -544,22 +432,20 @@ public function specifyTypesInCondition( $result = $result->unionWith( $this->create( $expr->left, - $orEqual ? $rightType->getSmallerOrEqualType() : $rightType->getSmallerType(), + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, - $scope - ) + $scope, + )->setRootExpr($expr), ); } if (!$expr->right instanceof Node\Scalar) { $result = $result->unionWith( $this->create( $expr->right, - $orEqual ? $leftType->getGreaterOrEqualType() : $leftType->getGreaterType(), + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, - $scope - ) + $scope, + )->setRootExpr($expr), ); } } elseif ($context->false()) { @@ -567,22 +453,20 @@ public function specifyTypesInCondition( $result = $result->unionWith( $this->create( $expr->left, - $orEqual ? $rightType->getGreaterType() : $rightType->getGreaterOrEqualType(), + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, - $scope - ) + $scope, + )->setRootExpr($expr), ); } if (!$expr->right instanceof Node\Scalar) { $result = $result->unionWith( $this->create( $expr->right, - $orEqual ? $leftType->getSmallerType() : $leftType->getSmallerOrEqualType(), + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, - $scope - ) + $scope, + )->setRootExpr($expr), ); } } @@ -590,40 +474,104 @@ public function specifyTypesInCondition( return $result; } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context); + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\GreaterOrEqual) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context); + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); } elseif ($expr instanceof FuncCall && $expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); + $normalizedExpr = $expr; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + } + foreach ($this->getFunctionTypeSpecifyingExtensions() as $extension) { - if (!$extension->isFunctionSupported($functionReflection, $expr, $context)) { + if (!$extension->isFunctionSupported($functionReflection, $normalizedExpr, $context)) { continue; } - return $extension->specifyTypes($functionReflection, $expr, $scope, $context); + return $extension->specifyTypes($functionReflection, $normalizedExpr, $scope, $context); + } + + if (count($expr->getArgs()) > 0) { + $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) { + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + + $normalizedExpr = $expr; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + } + + $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)) { + if (!$extension->isMethodSupported($methodReflection, $normalizedExpr, $context)) { continue; } - return $extension->specifyTypes($methodReflection, $expr, $scope, $context); + return $extension->specifyTypes($methodReflection, $normalizedExpr, $scope, $context); + } + } + + if (count($expr->getArgs()) > 0) { + $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; } } } @@ -638,240 +586,1048 @@ public function specifyTypesInCondition( $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { - $referencedClasses = TypeUtils::getDirectClassNames($calleeType); + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + + $normalizedExpr = $expr; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + } + + $referencedClasses = $calleeType->getObjectClassNames(); if ( count($referencedClasses) === 1 && $this->reflectionProvider->hasClass($referencedClasses[0]) ) { $staticMethodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); foreach ($this->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) { - if (!$extension->isStaticMethodSupported($staticMethodReflection, $expr, $context)) { + if (!$extension->isStaticMethodSupported($staticMethodReflection, $normalizedExpr, $context)) { continue; } - return $extension->specifyTypes($staticMethodReflection, $expr, $scope, $context); + return $extension->specifyTypes($staticMethodReflection, $normalizedExpr, $scope, $context); + } + } + + if (count($expr->getArgs()) > 0) { + $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); - $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes); + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + $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( + return (new SpecifiedTypes( $types->getSureTypes(), $types->getSureNotTypes(), - false, - array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes) - ) - ); + ))->setNewConditionalExpressionHolders(array_merge( + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), + ))->setRootExpr($expr); } return $types; } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) { - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); - $rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context); - $types = $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes); + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + $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( + return (new SpecifiedTypes( $types->getSureTypes(), $types->getSureNotTypes(), - false, - array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes) - ) - ); + ))->setNewConditionalExpressionHolders(array_merge( + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), + ))->setRootExpr($expr); } return $types; } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate()); + return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\Assign) { if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } + if ($context->null()) { - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context); + $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + + // infer $arr[$key] after $key = array_key_first/last($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first' + ? $arrayType->getFirstIterableValueType() + : $arrayType->getLastIterableValueType(); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), + ); + } + } + + // infer $list[$count] after $count = count($list) - 1 + if ( + $expr->expr instanceof Expr\BinaryOp\Minus + && $expr->expr->left instanceof FuncCall + && $expr->expr->left->name instanceof Name + && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($expr->expr->left->getArgs()) >= 1 + && $expr->expr->right instanceof Node\Scalar\Int_ + && $expr->expr->right->value === 1 + ) { + $arrayArg = $expr->expr->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + ); + } + } + + return $specifiedTypes; } - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context); + $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); + + if ($context->true()) { + // infer $arr[$key] after $key = array_search($needle, $arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && $expr->expr->name->toLowerString() === 'array_search' + && count($expr->expr->getArgs()) >= 2 + ) { + $arrayArg = $expr->expr->getArgs()[1]->value; + $arrayType = $scope->getType($arrayArg); + + if ($arrayType->isArray()->yes()) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $iterableValueType = $arrayType->getIterableValueType(); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), + ); + } + } + } + return $specifiedTypes; } elseif ( $expr instanceof Expr\Isset_ && count($expr->vars) > 0 - && $context->true() + && !$context->null() ) { - $vars = []; - foreach ($expr->vars as $var) { - $tmpVars = [$var]; + // rewrite multi param isset() to and-chained single param isset() + if (count($expr->vars) > 1) { + $issets = []; + foreach ($expr->vars as $var) { + $issets[] = new Expr\Isset_([$var], $expr->getAttributes()); + } - while ( - $var instanceof ArrayDimFetch - || $var instanceof PropertyFetch - || ( - $var instanceof StaticPropertyFetch - && $var->class instanceof Expr - ) - ) { - if ($var instanceof StaticPropertyFetch) { - /** @var Expr $var */ - $var = $var->class; - } else { - $var = $var->var; + $first = array_shift($issets); + $andChain = null; + foreach ($issets as $isset) { + if ($andChain === null) { + $andChain = new BooleanAnd($first, $isset); + continue; } - $tmpVars[] = $var; + + $andChain = new BooleanAnd($andChain, $isset); } - $vars = array_merge($vars, array_reverse($tmpVars)); - } + if ($andChain === null) { + throw new ShouldNotHappenException(); + } - if (count($vars) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr); } - $types = null; - foreach ($vars as $var) { + $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([], []); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } } + if ( $var instanceof ArrayDimFetch && $var->dim !== null && !$scope->getType($var->var) instanceof MixedType ) { - $type = $this->create( - $var->var, - new HasOffsetType($scope->getType($var->dim)), - $context, - false, - $scope - )->unionWith( - $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) - ); - } else { - $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope); + $dimType = $scope->getType($var->dim); + + if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { + $types = $types->unionWith( + $this->create( + $var->var, + new HasOffsetType($dimType), + $context, + $scope, + )->setRootExpr($expr), + ); + } else { + $varType = $scope->getType($var->var); + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); + if ($narrowedKey !== null) { + $types = $types->unionWith( + $this->create( + $var->dim, + $narrowedKey, + $context, + $scope, + )->setRootExpr($expr), + ); + } + } } if ( $var instanceof PropertyFetch && $var->name instanceof Node\Identifier ) { - $type = $type->unionWith($this->create($var->var, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope)); + $types = $types->unionWith( + $this->create($var->var, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), + ); } elseif ( $var instanceof StaticPropertyFetch && $var->class instanceof Expr && $var->name instanceof Node\VarLikeIdentifier ) { - $type = $type->unionWith($this->create($var->class, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope)); + $types = $types->unionWith( + $this->create($var->class, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), + ); } - if ($types === null) { - $types = $type; - } else { - $types = $types->unionWith($type); - } + $types = $types->unionWith( + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr), + ); } return $types; } elseif ( $expr instanceof Expr\BinaryOp\Coalesce - && $context->true() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right))->yes()) + && !$context->null() ) { - return $this->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - false, - $scope - ); + if (!$context->true()) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->left, static fn () => true); + + if ($isset !== true) { + return new SpecifiedTypes(); + } + + return $this->create( + $expr->left, + new NullType(), + $context->negate(), + $scope, + )->setRootExpr($expr); + } + + if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { + return $this->create( + $expr->left, + new NullType(), + TypeSpecifierContext::createFalse(), + $scope, + )->setRootExpr($expr); + } + } elseif ( $expr instanceof Expr\Empty_ ) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->expr, static fn () => true); + if ($isset === false) { + return new SpecifiedTypes(); + } + return $this->specifyTypesInCondition($scope, new BooleanOr( new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), - new Expr\BooleanNot($expr->expr) - ), $context); + new Expr\BooleanNot($expr->expr), + ), $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\ErrorSuppress) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context); + return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); } elseif ( $expr instanceof Expr\Ternary && !$context->null() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->else))->yes()) + && $scope->getType($expr->else)->isFalse()->yes() ) { $conditionExpr = $expr->cond; if ($expr->if !== null) { $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); } - return $this->specifyTypesInCondition($scope, $conditionExpr, $context); + return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) { $types = $this->specifyTypesInCondition( $scope, new BooleanAnd( new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new PropertyFetch($expr->var, $expr->name) + new PropertyFetch($expr->var, $expr->name), ), - $context - ); + $context, + )->setRootExpr($expr); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) { $types = $this->specifyTypesInCondition( $scope, new BooleanAnd( new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new MethodCall($expr->var, $expr->name, $expr->args) + new MethodCall($expr->var, $expr->name, $expr->args), ), - $context - ); + $context, + )->setRootExpr($expr); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); + } elseif ( + $expr instanceof Expr\New_ + && $expr->class instanceof Name + && $this->reflectionProvider->hasClass($expr->class->toString()) + ) { + $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); + + if ($classReflection->hasConstructor()) { + $methodReflection = $classReflection->getConstructor(); + $asserts = $methodReflection->getAsserts(); + + if ($asserts->getAll() !== []) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + } } elseif (!$context->null()) { return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } - return new SpecifiedTypes(); + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + private function specifyTypesForCountFuncCall( + FuncCall $countFuncCall, + Type $type, + Type $sizeType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes + { + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate()); + } + + $isConstantArray = $type->isConstantArray(); + $isList = $type->isList(); + $oneOrMore = IntegerRangeType::fromInterval(1, null); + if ( + !$isNormalCount->yes() + || (!$isConstantArray->yes() && !$isList->yes()) + || !$oneOrMore->isSuperTypeOf($sizeType)->yes() + || $sizeType->isSuperTypeOf($type->getArraySize())->yes() + ) { + return null; + } + + $resultTypes = []; + foreach ($type->getArrays() as $arrayType) { + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize()); + if ($isSizeSuperTypeOfArraySize->no()) { + continue; + } + + if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { + continue; + } + + if ( + $sizeType instanceof ConstantIntegerType + && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $isList->yes() + && $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 + && $isList->yes() + && $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[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + 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); + } + + if ( + $context->false() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && in_array(strtolower((string) $exprNode->name), [ + 'trim', 'ltrim', 'rtrim', + 'mb_trim', 'mb_ltrim', 'mb_rtrim', + ], true) + && isset($exprNode->getArgs()[0]) + && $constantStringValue === '' + ) { + $argValue = $exprNode->getArgs()[0]->value; + $argType = $scope->getType($argValue); + if ($argType->isString()->yes()) { + return $this->create( + $argValue, + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + $context->negate(), + $scope, + )->setRootExpr($rootExpr); + } + } + + return null; } private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes { if ($context->null()) { - return new SpecifiedTypes(); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } if (!$context->truthy()) { $type = StaticTypeFactory::truthy(); - return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); } elseif (!$context->falsey()) { $type = StaticTypeFactory::falsey(); - return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); + } + + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + private function specifyTypesFromConditionalReturnType( + TypeSpecifierContext $context, + Expr\CallLike $call, + ParametersAcceptor $parametersAcceptor, + Scope $scope, + ): ?SpecifiedTypes + { + if (!$parametersAcceptor instanceof ResolvedFunctionVariant) { + return null; + } + + $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType(); + if (!$returnType instanceof ConditionalTypeForParameter) { + return null; + } + + if ($context->true()) { + $leftType = new ConstantBooleanType(true); + $rightType = new ConstantBooleanType(false); + } elseif ($context->false()) { + $leftType = new ConstantBooleanType(false); + $rightType = new ConstantBooleanType(true); + } elseif ($context->null()) { + $leftType = new MixedType(); + $rightType = new NeverType(); + } else { + return null; + } + + $argsMap = []; + $parameters = $parametersAcceptor->getParameters(); + foreach ($call->getArgs() as $i => $arg) { + if ($arg->unpack) { + continue; + } + + if ($arg->name !== null) { + $paramName = $arg->name->toString(); + } elseif (isset($parameters[$i])) { + $paramName = $parameters[$i]->getName(); + } else { + continue; + } + + $argsMap['$' . $paramName] = $arg->value; + } + + return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argsMap); + } + + /** + * @param array $argsMap + */ + public function getConditionalSpecifiedTypes( + ConditionalTypeForParameter $conditionalType, + Type $leftType, + Type $rightType, + Scope $scope, + array $argsMap, + ): ?SpecifiedTypes + { + $parameterName = $conditionalType->getParameterName(); + if (!array_key_exists($parameterName, $argsMap)) { + return null; + } + + $targetType = $conditionalType->getTarget(); + $ifType = $conditionalType->getIf(); + $elseType = $conditionalType->getElse(); + + if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) { + $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(); + } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) { + $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse(); + } else { + return null; + } + + $specifiedTypes = $this->create( + $argsMap[$parameterName], + $targetType, + $context, + $scope, + ); + + if ($targetType instanceof ConstantBooleanType) { + if (!$targetType->getValue()) { + $context = $context->negate(); + } + + $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argsMap[$parameterName], $context)); + } + + return $specifiedTypes; + } + + private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes + { + if ($context->null()) { + $asserts = $assertions->getAsserts(); + } elseif ($context->true()) { + $asserts = $assertions->getAssertsIfTrue(); + } elseif ($context->false()) { + $asserts = $assertions->getAssertsIfFalse(); + } else { + throw new ShouldNotHappenException(); + } + + if (count($asserts) === 0) { + return null; + } + + $argsMap = []; + $parameters = $parametersAcceptor->getParameters(); + foreach ($call->getArgs() as $i => $arg) { + if ($arg->unpack) { + continue; + } + + if ($arg->name !== null) { + $paramName = $arg->name->toString(); + } elseif (isset($parameters[$i])) { + $paramName = $parameters[$i]->getName(); + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $lastParameter = $parameters[count($parameters) - 1]; + $paramName = $lastParameter->getName(); + } else { + continue; + } + + $argsMap[$paramName][] = $arg->value; + } + foreach ($parameters as $parameter) { + $name = $parameter->getName(); + $defaultValue = $parameter->getDefaultValue(); + if (isset($argsMap[$name]) || $defaultValue === null) { + continue; + } + $argsMap[$name][] = new TypeExpr($defaultValue); + } + + 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 new SpecifiedTypes(); + return $types; } /** - * @param Scope $scope - * @param SpecifiedTypes $leftTypes - * @param SpecifiedTypes $rightTypes * @return array */ - private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + { + $conditionExpressionTypes = []; + foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::remove($scope->getType($expr), $type), + ); + } + + if (count($conditionExpressionTypes) > 0) { + $holders = []; + foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + if (!isset($holders[$exprString])) { + $holders[$exprString] = []; + } + + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + + $holder = new ConditionalExpressionHolder( + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::intersect($scope->getType($expr), $type), TrinaryLogic::createYes()), + ); + $holders[$exprString][$holder->getKey()] = $holder; + } + + return $holders; + } + + return []; + } + + /** + * @return array + */ + private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { @@ -882,7 +1638,10 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le continue; } - $conditionExpressionTypes[$exprString] = TypeCombinator::intersect($scope->getType($expr), $type); + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::intersect($scope->getType($expr), $type), + ); } if (count($conditionExpressionTypes) > 0) { @@ -899,10 +1658,31 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le $holders[$exprString] = []; } - $holders[$exprString][] = new ConditionalExpressionHolder( - $conditionExpressionTypes, - new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()) + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + + $holder = new ConditionalExpressionHolder( + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), ); + $holders[$exprString][$holder->getKey()] = $holder; } return $holders; @@ -912,61 +1692,113 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr\BinaryOp $binaryOperation - * @return (Expr|\PHPStan\Type\ConstantScalarType)[]|null + * @return array{Expr, ConstantScalarType, Type}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { $leftType = $scope->getType($binaryOperation->left); $rightType = $scope->getType($binaryOperation->right); + + $rightExpr = $binaryOperation->right; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightExpr = $rightExpr->getExpr(); + } + + $leftExpr = $binaryOperation->left; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftExpr = $leftExpr->getExpr(); + } + if ( - $leftType instanceof \PHPStan\Type\ConstantScalarType - && !$binaryOperation->right instanceof ConstFetch - && !$binaryOperation->right instanceof Expr\ClassConstFetch + $leftType instanceof ConstantScalarType + && !$rightExpr instanceof ConstFetch ) { - 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 ) { - return [$binaryOperation->left, $rightType]; + return [$binaryOperation->left, $rightType, $leftType]; + } + + return null; + } + + /** @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; } - return null; + $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; } - /** @api */ - public function create( + private function createForExpr( Expr $expr, Type $type, TypeSpecifierContext $context, - bool $overwrite = false, - ?Scope $scope = null + Scope $scope, ): SpecifiedTypes { - if ($expr instanceof New_ || $expr instanceof Instanceof_ || $expr instanceof Expr\List_) { - return new 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(); } - while ($expr instanceof Expr\Assign) { - $expr = $expr->var; + $originalExpr = $expr; + if (isset($containsNull) && !$containsNull) { + $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr); } - if ($scope !== null) { - if ($context->true()) { - $resultType = TypeCombinator::intersect($scope->getType($expr), $type); - } elseif ($context->false()) { - $resultType = TypeCombinator::remove($scope->getType($expr), $type); + if ( + !$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; } } - $originalExpr = $expr; - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { - $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr); - } - if ( $expr instanceof FuncCall && $expr->name instanceof Name @@ -974,43 +1806,83 @@ public function create( $has = $this->reflectionProvider->hasFunction($expr->name, $scope); if (!$has) { // backwards compatibility with previous behaviour - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); } $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); - if ($functionReflection->hasSideEffects()->yes()) { - return new SpecifiedTypes(); + $hasSideEffects = $functionReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return new SpecifiedTypes([], []); + } + + if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) { + return new SpecifiedTypes([], []); } } if ( $expr instanceof MethodCall && $expr->name instanceof Node\Identifier - && $scope !== null ) { $methodName = $expr->name->toString(); $calledOnType = $scope->getType($expr->var); $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); - if ($methodReflection === null || $methodReflection->hasSideEffects()->yes()) { - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if ( + $methodReflection === null + || $methodReflection->hasSideEffects()->yes() + || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) + ) { + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); } - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); + } + } + + if ( + $expr instanceof StaticCall + && $expr->name instanceof Node\Identifier + ) { + $methodName = $expr->name->toString(); + if ($expr->class instanceof Name) { + $calledOnType = $scope->resolveTypeByName($expr->class); + } else { + $calledOnType = $scope->getType($expr->class); + } + + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ( + $methodReflection === null + || $methodReflection->hasSideEffects()->yes() + || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) + ) { + if (isset($containsNull) && !$containsNull) { + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); + } + + return new SpecifiedTypes([], []); } } $sureTypes = []; $sureNotTypes = []; - $exprString = $this->printer->prettyPrintExpr($expr); + $exprString = $this->exprPrinter->printExpr($expr); + $originalExprString = $this->exprPrinter->printExpr($originalExpr); if ($context->false()) { $sureNotTypes[$exprString] = [$expr, $type]; + if ($exprString !== $originalExprString) { + $sureNotTypes[$originalExprString] = [$originalExpr, $type]; + } } elseif ($context->true()) { $sureTypes[$exprString] = [$expr, $type]; + if ($exprString !== $originalExprString) { + $sureTypes[$originalExprString] = [$originalExpr, $type]; + } } - $types = new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite); - if ($scope !== null && isset($resultType) && !TypeCombinator::containsNull($resultType)) { + $types = new SpecifiedTypes($sureTypes, $sureNotTypes); + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types); } @@ -1021,25 +1893,25 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont { if ($expr instanceof Expr\NullsafePropertyFetch) { if ($type !== null) { - $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, false, $scope); + $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope); } else { - $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), false, $scope); + $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope); } return $propertyFetchTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope), ); } if ($expr instanceof Expr\NullsafeMethodCall) { if ($type !== null) { - $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, false, $scope); + $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope); } else { - $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), false, $scope); + $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope); } return $methodCallTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope), ); } @@ -1063,15 +1935,15 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont return $this->createNullsafeTypes($expr->class, $scope, $context, null); } - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); } - private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes + private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes { $sureNotTypes = []; if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $exprString = $this->printer->prettyPrintExpr($expr); + $exprString = $this->exprPrinter->printExpr($expr); if ($context->false()) { $sureNotTypes[$exprString] = [$expr, $type]; } elseif ($context->true()) { @@ -1080,11 +1952,11 @@ private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $ } } - return new SpecifiedTypes([], $sureNotTypes); + return (new SpecifiedTypes(sureNotTypes: $sureNotTypes))->setRootExpr($rootExpr); } /** - * @return \PHPStan\Type\FunctionTypeSpecifyingExtension[] + * @return FunctionTypeSpecifyingExtension[] */ private function getFunctionTypeSpecifyingExtensions(): array { @@ -1092,8 +1964,7 @@ private function getFunctionTypeSpecifyingExtensions(): array } /** - * @param string $className - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] + * @return MethodTypeSpecifyingExtension[] */ private function getMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -1109,8 +1980,7 @@ private function getMethodTypeSpecifyingExtensionsForClass(string $className): a } /** - * @param string $className - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] + * @return StaticMethodTypeSpecifyingExtension[] */ private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -1126,8 +1996,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 @@ -1145,4 +2014,580 @@ 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->true() + && $exprNode instanceof ClassConstFetch + && $exprNode->name instanceof Node\Identifier + && strtolower($exprNode->name->toString()) === 'class' + && $constantType->isString()->yes() + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); + } + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + $leftBooleanType = $leftType->toBoolean(); + if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), + $expr->right, + ), + $context, + )->setRootExpr($expr); + } + + $rightBooleanType = $rightType->toBoolean(); + if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + $expr->left, + new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), + ), + $context, + )->setRootExpr($expr); + } + + if ( + !$context->null() + && $rightType->isArray()->yes() + && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + + if ( + !$context->null() + && $leftType->isArray()->yes() + && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + + if ( + ($leftType->isString()->yes() && $rightType->isString()->yes()) + || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) + || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) + || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); + } + + $leftExprString = $this->exprPrinter->printExpr($expr->left); + $rightExprString = $this->exprPrinter->printExpr($expr->right); + if ($leftExprString === $rightExprString) { + if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } + + $leftTypes = $this->create($expr->left, $leftType, $context, $scope)->setRootExpr($expr); + $rightTypes = $this->create($expr->right, $rightType, $context, $scope)->setRootExpr($expr); + + return $context->true() + ? $leftTypes->unionWith($rightTypes) + : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + } + + public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + // Normalize to: fn() === expr + $leftExpr = $expr->left; + $rightExpr = $expr->right; + if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { + [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; + } + + $unwrappedLeftExpr = $leftExpr; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $unwrappedLeftExpr = $leftExpr->getExpr(); + } + $unwrappedRightExpr = $rightExpr; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $unwrappedRightExpr = $rightExpr->getExpr(); + } + + $rightType = $scope->getType($rightExpr); + + // (count($a) === $b) + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) >= 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); + } + + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + + if ($context->truthy() && !$argType->isArray()->yes()) { + $newArgType = new UnionType([ + new ObjectType(Countable::class), + new ConstantArrayType([], []), + ]); + } else { + $newArgType = new ConstantArrayType([], []); + } + + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, $scope)->setRootExpr($expr), + ); + } + + $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + + if ($context->truthy() && $argType->isArray()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr), + ); + } + + return $funcTypes; + } + } + + // strlen($a) === $b + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) === 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, $scope)->setRootExpr($expr), + ); + } + + if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + + $accessory = new AccessoryNonEmptyStringType(); + if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr); + + return $funcTypes->unionWith($valueTypes); + } + } + } + + // preg_match($a) === $b + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && $unwrappedLeftExpr->name->toLowerString() === 'preg_match' + && (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes() + ) { + return $this->specifyTypesInCondition( + $scope, + $leftExpr, + $context, + )->setRootExpr($expr); + } + + // get_class($a) === 'Foo' + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true) + && isset($unwrappedLeftExpr->getArgs()[0]) + ) { + if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + new ObjectType($rightType->getValue(), classReflection: $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(), classReflection: $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(), classReflection: $this->reflectionProvider->getClass($leftType->getValue())->asFinal()), + $context, + $scope, + )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); + } + + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $unwrappedRightExpr->class, + new Name($leftType->getValue()), + ), + $context, + )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); + } + + if ($context->false()) { + $identicalType = $scope->getType($expr); + if ($identicalType instanceof ConstantBooleanType) { + $never = new NeverType(); + $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; + $leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr); + $rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), + ); + } + return $leftTypes->unionWith($rightTypes); + } + } + + $types = null; + if ( + count($leftType->getFiniteTypes()) === 1 + || ( + $context->true() + && $leftType->isConstantValue()->yes() + && !$rightType->equals($leftType) + && $rightType->isSuperTypeOf($leftType)->yes()) + ) { + $types = $this->create( + $rightExpr, + $leftType, + $context, + $scope, + )->setRootExpr($expr); + if ($rightExpr instanceof AlwaysRememberedExpr) { + $types = $types->unionWith($this->create( + $unwrappedRightExpr, + $leftType, + $context, + $scope, + ))->setRootExpr($expr); + } + } + if ( + count($rightType->getFiniteTypes()) === 1 + || ( + $context->true() + && $rightType->isConstantValue()->yes() + && !$leftType->equals($rightType) + && $leftType->isSuperTypeOf($rightType)->yes() + ) + ) { + $leftTypes = $this->create( + $leftExpr, + $rightType, + $context, + $scope, + )->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith($this->create( + $unwrappedLeftExpr, + $rightType, + $context, + $scope, + ))->setRootExpr($expr); + } + if ($types !== null) { + $types = $types->unionWith($leftTypes); + } else { + $types = $leftTypes; + } + } + + if ($types !== null) { + return $types; + } + + $leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr); + $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr); + if ($leftExprString === $rightExprString) { + if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } + + if ($context->true()) { + $leftTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); + $rightTypes = $this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $leftType, $context, $scope)->setRootExpr($expr), + ); + } + return $leftTypes->unionWith($rightTypes); + } elseif ($context->false()) { + return $this->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope) + ->intersectWith($this->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope)); + } + + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index bbebcf18e6..d7379c26ac 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -2,8 +2,12 @@ namespace PHPStan\Analyser; -/** @api */ -class TypeSpecifierContext +use PHPStan\ShouldNotHappenException; + +/** + * @api + */ +final class TypeSpecifierContext { public const CONTEXT_TRUE = 0b0001; @@ -14,20 +18,18 @@ class TypeSpecifierContext public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE; public const CONTEXT_BITMASK = 0b1111; - private ?int $value; - /** @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); - return self::$registry[$value]; + $key = $value ?? ''; + self::$registry[$key] ??= new self($value); + return self::$registry[$key]; } public static function createTrue(): self @@ -58,7 +60,7 @@ 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 & self::CONTEXT_BITMASK); } diff --git a/src/Analyser/TypeSpecifierFactory.php b/src/Analyser/TypeSpecifierFactory.php index 1873493047..5df7f61e07 100644 --- a/src/Analyser/TypeSpecifierFactory.php +++ b/src/Analyser/TypeSpecifierFactory.php @@ -2,33 +2,36 @@ namespace PHPStan\Analyser; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; +use function array_merge; -class TypeSpecifierFactory +#[AutowiredService(name: '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 +39,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..a6e805d69a 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -2,18 +2,21 @@ namespace PHPStan\Analyser; -class UndefinedVariableException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use function sprintf; + +/** + * @api + * + * Unchecked exception thrown from `PHPStan\Analyser\Scope::getVariableType()` + * in case the user doesn't check `hasVariableType()` is not `no()`. + */ +final class UndefinedVariableException extends AnalysedCodeException { - private \PHPStan\Analyser\Scope $scope; - - private string $variableName; - - 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 aba3d385d7..0000000000 --- a/src/Analyser/VariableTypeHolder.php +++ /dev/null @@ -1,64 +0,0 @@ -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 equals(self $other): bool - { - if (!$this->certainty->equals($other->certainty)) { - return false; - } - - return $this->type->equals($other->type); - } - - public function and(self $other): self - { - if ($this->getType()->equals($other->getType())) { - $type = $this->getType(); - } else { - $type = TypeCombinator::union($this->getType(), $other->getType()); - } - return new self( - $type, - $this->getCertainty()->and($other->getCertainty()) - ); - } - - public function getType(): Type - { - return $this->type; - } - - public function getCertainty(): TrinaryLogic - { - return $this->certainty; - } - -} diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index b7040689a2..5ca264ab16 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -2,41 +2,55 @@ namespace PHPStan\Broker; +use PhpParser\Node; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; +use PHPStan\ShouldNotHappenException; +use function md5; +use function sprintf; -class AnonymousClassNameHelper +#[AutowiredService] +final class AnonymousClassNameHelper { - private FileHelper $fileHelper; - - private RelativePathHelper $relativePathHelper; - public function __construct( - FileHelper $fileHelper, - RelativePathHelper $relativePathHelper + private FileHelper $fileHelper, + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + 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 d46f30576b..0000000000 --- a/src/Broker/Broker.php +++ /dev/null @@ -1,149 +0,0 @@ -reflectionProvider = $reflectionProvider; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; - } - - public static function registerInstance(Broker $broker): void - { - self::$instance = $broker; - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProviderStaticAccessor instead - */ - public static function getInstance(): Broker - { - if (self::$instance === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - return self::$instance; - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasClass(string $className): bool - { - return $this->reflectionProvider->hasClass($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getClass(string $className): ClassReflection - { - return $this->reflectionProvider->getClass($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getClassName(string $className): string - { - return $this->reflectionProvider->getClassName($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function supportsAnonymousClasses(): bool - { - return $this->reflectionProvider->supportsAnonymousClasses(); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $scope); - } - - /** - * @deprecated Inject %universalObjectCratesClasses% parameter instead. - * - * @return string[] - */ - public function getUniversalObjectCratesClasses(): array - { - return $this->universalObjectCratesClasses; - } - -} diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index f8a937c20a..bbd8d97a3d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -2,32 +2,16 @@ namespace PHPStan\Broker; -use PHPStan\DependencyInjection\Container; -use PHPStan\Reflection\ReflectionProvider; - -class BrokerFactory +final class BrokerFactory { public const PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.propertiesClassReflectionExtension'; public const METHODS_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.methodsClassReflectionExtension'; + public const ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.allowedSubTypesClassReflectionExtension'; public const DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicMethodReturnTypeExtension'; public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension'; public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension'; public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; - - 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->getParameter('universalObjectCratesClasses') - ); - } + public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; } diff --git a/src/Broker/ClassAutoloadingException.php b/src/Broker/ClassAutoloadingException.php deleted file mode 100644 index 9898953138..0000000000 --- a/src/Broker/ClassAutoloadingException.php +++ /dev/null @@ -1,42 +0,0 @@ -getMessage(), - $functionName - ), 0, $previous); - } else { - parent::__construct(sprintf( - 'Class %s not found.', - $functionName - ), 0); - } - - $this->className = $functionName; - } - - public function getClassName(): string - { - return $this->className; - } - - 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..0cabab122c 100644 --- a/src/Broker/ClassNotFoundException.php +++ b/src/Broker/ClassNotFoundException.php @@ -2,15 +2,22 @@ namespace PHPStan\Broker; -class ClassNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $className; +/** + * @api + * + * Unchecked exception thrown from `ReflectionProvider` and other places + * in case the user does not check the existence of the class beforehand + * with `hasClass()` or similar. + */ +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 +25,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..41981f07d8 100644 --- a/src/Broker/ConstantNotFoundException.php +++ b/src/Broker/ConstantNotFoundException.php @@ -2,15 +2,22 @@ namespace PHPStan\Broker; -class ConstantNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $constantName; +/** + * @api + * + * Unchecked exception thrown from `ReflectionProvider` + * in case the user does not check the existence of the constant beforehand + * with `hasConstant()`. + */ +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 +25,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..9607608462 100644 --- a/src/Broker/FunctionNotFoundException.php +++ b/src/Broker/FunctionNotFoundException.php @@ -2,15 +2,22 @@ namespace PHPStan\Broker; -class FunctionNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $functionName; +/** + * @api + * + * Unchecked exception thrown from `ReflectionProvider` + * in case the user does not check the existence of the function beforehand + * with `hasFunction()`. + */ +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 +25,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..8f4deb6f13 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -2,18 +2,21 @@ namespace PHPStan\Cache; -class Cache -{ +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; - private \PHPStan\Cache\CacheStorage $storage; +#[AutowiredService] +final class Cache +{ - public function __construct(CacheStorage $storage) + public function __construct( + #[AutowiredParameter(ref: '@cacheStorage')] + private CacheStorage $storage, + ) { - $this->storage = $storage; } /** - * @param string $key * @return mixed|null */ public function load(string $key, string $variableKey) @@ -22,10 +25,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 709cbdf00d..1b66f26e2a 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -2,40 +2,47 @@ 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'; - private function makeDir(string $directory): void + public function __construct(private string $directory) { - if (is_dir($directory)) { - return; - } - - $result = @mkdir($directory, 0777); - if ($result === false) { - clearstatcache(); - if (is_dir($directory)) { - return; - } - - $error = error_get_last(); - throw new \InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); - } } /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) @@ -43,11 +50,7 @@ public function load(string $key, string $variableKey) [,, $filePath] = $this->getFilePaths($key); return (static function () use ($variableKey, $filePath) { - if (!is_file($filePath)) { - return null; - } - - $cacheItem = require $filePath; + $cacheItem = @include $filePath; if (!$cacheItem instanceof CacheItem) { return null; } @@ -60,31 +63,30 @@ public function load(string $key, string $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 \PHPStan\ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); + throw new ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); } FileWriter::write( $tmpPath, sprintf( - "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..ae13a94614 --- /dev/null +++ b/src/Collectors/CollectedData.php @@ -0,0 +1,91 @@ +>, 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] + #[Override] + 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..c0ff3dd404 --- /dev/null +++ b/src/Collectors/Registry.php @@ -0,0 +1,58 @@ +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..a95dbe2a51 --- /dev/null +++ b/src/Collectors/RegistryFactory.php @@ -0,0 +1,25 @@ +container->getServicesByTag(self::COLLECTOR_TAG), + ); + } + +} diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 1618d2eb28..53168f8edd 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -3,54 +3,48 @@ namespace PHPStan\Command; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\Collectors\CollectedData; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\BytesHelper; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\PhpDoc\StubValidator; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\InputInterface; +use function array_merge; +use function count; +use function is_file; +use function memory_get_peak_usage; +use function microtime; +use function sha1_file; +use function sprintf; -class AnalyseApplication +/** + * @phpstan-import-type CollectorData from CollectedData + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ +#[AutowiredService] +final class AnalyseApplication { - private AnalyserRunner $analyserRunner; - - private \PHPStan\PhpDoc\StubValidator $stubValidator; - - private \PHPStan\Analyser\ResultCache\ResultCacheManagerFactory $resultCacheManagerFactory; - - private IgnoredErrorHelper $ignoredErrorHelper; - - private string $memoryLimitFile; - - private int $internalErrorsCountLimit; - public function __construct( - AnalyserRunner $analyserRunner, - StubValidator $stubValidator, - ResultCacheManagerFactory $resultCacheManagerFactory, - IgnoredErrorHelper $ignoredErrorHelper, - string $memoryLimitFile, - int $internalErrorsCountLimit + private AnalyserRunner $analyserRunner, + private AnalyserResultFinalizer $analyserResultFinalizer, + private StubValidator $stubValidator, + private ResultCacheManagerFactory $resultCacheManagerFactory, + private IgnoredErrorHelper $ignoredErrorHelper, + private StubFilesProvider $stubFilesProvider, ) { - $this->analyserRunner = $analyserRunner; - $this->stubValidator = $stubValidator; - $this->resultCacheManagerFactory = $resultCacheManagerFactory; - $this->ignoredErrorHelper = $ignoredErrorHelper; - $this->memoryLimitFile = $memoryLimitFile; - $this->internalErrorsCountLimit = $internalErrorsCountLimit; } /** * @param string[] $files - * @param bool $onlyFiles - * @param \PHPStan\Command\Output $stdOutput - * @param \PHPStan\Command\Output $errorOutput - * @param bool $defaultLevelUsed - * @param bool $debug - * @param string|null $projectConfigFile * @param mixed[]|null $projectConfigArray - * @return AnalysisResult */ public function analyse( array $files, @@ -61,42 +55,30 @@ public function analyse( bool $debug, ?string $projectConfigFile, ?array $projectConfigArray, - InputInterface $input + ?string $tmpFile, + ?string $insteadOfFile, + InputInterface $input, ): AnalysisResult { - $this->updateMemoryLimitFile(); - $projectStubFiles = []; - if ($projectConfigArray !== null) { - $projectStubFiles = $projectConfigArray['parameters']['stubFiles'] ?? []; + $isResultCacheUsed = false; + $fileReplacements = []; + if ($tmpFile !== null && $insteadOfFile !== null) { + $fileReplacements = [$insteadOfFile => $tmpFile]; } - $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); - - register_shutdown_function(function (): void { - $error = error_get_last(); - if ($error === null) { - return; - } - if ($error['type'] !== E_ERROR) { - return; - } - - if (strpos($error['message'], 'Allowed memory size') !== false) { - return; - } - - @unlink($this->memoryLimitFile); - }); - - $resultCacheManager = $this->resultCacheManagerFactory->create([]); + $resultCacheManager = $this->resultCacheManagerFactory->create($fileReplacements); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + $fileSpecificErrors = []; if (count($ignoredErrorHelperResult->getErrors()) > 0) { - $errors = $ignoredErrorHelperResult->getErrors(); + $notFileSpecificErrors = $ignoredErrorHelperResult->getErrors(); $internalErrors = []; + $collectedData = []; $savedResultCache = false; - if ($errorOutput->isDebug()) { + $memoryUsageBytes = memory_get_peak_usage(true); + if ($errorOutput->isVeryVerbose()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( @@ -104,32 +86,85 @@ public function analyse( $files, $debug, $projectConfigFile, + $tmpFile, + $insteadOfFile, $stdOutput, $errorOutput, - $input + $input, ); + + $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->getUsedTraitDependencies(), + $intermediateAnalyserResult->getExportedNodes(), + $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), + $intermediateAnalyserResult->getPeakMemoryUsageBytes(), + ); + } + $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); - $analyserResult = $resultCacheResult->getAnalyserResult(); + $analyserResult = $this->analyserResultFinalizer->finalize( + $this->switchTmpFileInAnalyserResult($resultCacheResult->getAnalyserResult(), $insteadOfFile, $tmpFile), + $onlyFiles, + $debug, + )->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); - $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $files, count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit()); - $savedResultCache = $resultCacheResult->isSaved(); - if ($analyserResult->hasReachedInternalErrorsCountLimit()) { - $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); - } - $errors = array_merge($errors, $internalErrors); - } + $errors = array_merge( + $analyserResult->getErrors(), + $analyserResult->getFilteredPhpErrors(), + ); + $hasInternalErrors = count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); + $memoryUsageBytes = $analyserResult->getPeakMemoryUsageBytes(); + $isResultCacheUsed = !$resultCache->isFullAnalysis(); - $errors = array_merge($stubErrors, $errors); + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; + if ( + $isResultCacheUsed + && $resultCacheResult->isSaved() + && !$onlyFiles + && $projectConfigArray !== null + ) { + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } - $fileSpecificErrors = []; - $notFileSpecificErrors = []; - foreach ($errors as $error) { - if (is_string($error)) { - $notFileSpecificErrors[] = $error; - 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( @@ -137,12 +172,32 @@ public function analyse( $notFileSpecificErrors, $internalErrors, [], + $this->mapCollectedData($collectedData), $defaultLevelUsed, $projectConfigFile, - $savedResultCache + $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 @@ -152,9 +207,11 @@ private function runAnalyser( array $allAnalysedFiles, bool $debug, ?string $projectConfigFile, + ?string $tmpFile, + ?string $insteadOfFile, Output $stdOutput, Output $errorOutput, - InputInterface $input + InputInterface $input, ): AnalyserResult { $filesCount = count($files); @@ -163,56 +220,176 @@ private function runAnalyser( $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount); $errorOutput->getStyle()->progressFinish(); - return new AnalyserResult([], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } if (!$debug) { - $progressStarted = false; - $fileOrder = 0; $preFileCallback = null; - $postFileCallback = function (int $step) use ($errorOutput, &$progressStarted, $allAnalysedFilesCount, $filesCount, &$fileOrder): 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 ($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): void { + $postFileCallback = static function () use ($stdOutput, &$previousMemory, &$startTime): void { + if ($startTime === null) { + throw new ShouldNotHappenException(); + } $currentTotalMemory = memory_get_peak_usage(true); - $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory))); + $elapsedTime = microtime(true) - $startTime; + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime)); $previousMemory = $currentTotalMemory; }; } } - $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, null, null, $input); + $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $tmpFile, $insteadOfFile, $input); - if (isset($progressStarted) && $progressStarted) { + if (!$debug) { $errorOutput->getStyle()->progressFinish(); } return $analyserResult; } - private function updateMemoryLimitFile(): void + private function switchTmpFileInAnalyserResult( + AnalyserResult $analyserResult, + ?string $insteadOfFile, + ?string $tmpFile, + ): AnalyserResult + { + if ($insteadOfFile === null || $tmpFile === null) { + return $analyserResult; + } + + $newCollectedData = []; + foreach ($analyserResult->getCollectedData() as $file => $data) { + if ($file === $tmpFile) { + $file = $insteadOfFile; + } + + $newCollectedData[$file] = $data; + } + + $dependencies = null; + if ($analyserResult->getDependencies() !== null) { + $dependencies = $this->switchTmpFileInDependencies($analyserResult->getDependencies(), $insteadOfFile, $tmpFile); + } + $usedTraitDependencies = null; + if ($analyserResult->getUsedTraitDependencies() !== null) { + $usedTraitDependencies = $this->switchTmpFileInDependencies($analyserResult->getUsedTraitDependencies(), $insteadOfFile, $tmpFile); + } + + $exportedNodes = []; + foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { + if ($file === $tmpFile) { + $file = $insteadOfFile; + } + + $exportedNodes[$file] = $fileExportedNodes; + } + + return new AnalyserResult( + $this->switchTmpFileInErrors($analyserResult->getUnorderedErrors(), $insteadOfFile, $tmpFile), + $this->switchTmpFileInErrors($analyserResult->getFilteredPhpErrors(), $insteadOfFile, $tmpFile), + $this->switchTmpFileInErrors($analyserResult->getAllPhpErrors(), $insteadOfFile, $tmpFile), + $this->switchTmpFileInErrors($analyserResult->getLocallyIgnoredErrors(), $insteadOfFile, $tmpFile), + $this->switchTmpFileInLinesToIgnore($analyserResult->getLinesToIgnore(), $insteadOfFile, $tmpFile), + $this->switchTmpFileInLinesToIgnore($analyserResult->getUnmatchedLineIgnores(), $insteadOfFile, $tmpFile), + $analyserResult->getInternalErrors(), + $newCollectedData, + $dependencies, + $usedTraitDependencies, + $exportedNodes, + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ); + } + + /** + * @param array> $dependencies + * @return array> + */ + private function switchTmpFileInDependencies(array $dependencies, string $insteadOfFile, string $tmpFile): array { - $bytes = memory_get_peak_usage(true); - $megabytes = ceil($bytes / 1024 / 1024); - file_put_contents($this->memoryLimitFile, sprintf('%d MB', $megabytes)); + $newDependencies = []; + foreach ($dependencies as $dependencyFile => $dependentFiles) { + $new = []; + foreach ($dependentFiles as $file) { + if ($file === $tmpFile) { + $new[] = $insteadOfFile; + continue; + } + + $new[] = $file; + } + + $key = $dependencyFile; + if ($key === $tmpFile) { + $key = $insteadOfFile; + } + + $newDependencies[$key] = $new; + } + + return $newDependencies; + } + + /** + * @param list $errors + * @return list + */ + private function switchTmpFileInErrors(array $errors, string $insteadOfFile, string $tmpFile): array + { + $newErrors = []; + foreach ($errors as $error) { + if ($error->getFilePath() === $tmpFile) { + $error = $error->changeFilePath($insteadOfFile); + } + if ($error->getTraitFilePath() === $tmpFile) { + $error = $error->changeTraitFilePath($insteadOfFile); + } + + $newErrors[] = $error; + } + + return $newErrors; + } + + /** + * @param array $linesToIgnore + * @return array + */ + private function switchTmpFileInLinesToIgnore(array $linesToIgnore, string $insteadOfFile, string $tmpFile): array + { + $newLinesToIgnore = []; + foreach ($linesToIgnore as $file => $lines) { + if ($file === $tmpFile) { + $file = $insteadOfFile; + } + + $newLines = []; + foreach ($lines as $f => $line) { + if ($f === $tmpFile) { + $f = $insteadOfFile; + } + + $newLines[$f] = $line; + } + + $newLinesToIgnore[$file] = $newLines; + } + + return $newLinesToIgnore; } } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 4f54ceec3c..f14d289baf 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,23 +3,70 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; -use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use Override; +use PHPStan\Analyser\InternalError; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; +use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; -use PHPStan\Command\ErrorFormatter\TableErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\DependencyInjection\Container; +use PHPStan\Diagnose\DiagnoseExtension; +use PHPStan\Diagnose\PHPStanDiagnoseExtension; +use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileHelper; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\File\ParentDirectoryRelativePathHelper; +use PHPStan\File\PathNotFoundException; +use PHPStan\File\RelativePathHelper; +use PHPStan\Fixable\FileChangedException; +use PHPStan\Fixable\MergeConflictException; +use PHPStan\Fixable\Patcher; +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'; @@ -28,62 +75,66 @@ 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; } + #[Override] protected function configure(): void { $this->setName(self::NAME) ->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(ErrorsConsoleStyle::OPTION_NO_PROGRESS, mode: InputOption::VALUE_NONE, description: 'Do not show progress bar, only results'), + new InputOption('debug', mode: InputOption::VALUE_NONE, description: 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('error-format', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', null), - new InputOption('generate-baseline', null, InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), - new InputOption('fix', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), - new InputOption('watch', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), - new InputOption('pro', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), + new InputOption('error-format', mode: InputOption::VALUE_REQUIRED, description: 'Format in which to print the result of the analysis'), + new InputOption('generate-baseline', 'b', InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), + new InputOption('allow-empty-baseline', mode: InputOption::VALUE_NONE, description: 'Do not error out when the generated baseline is empty'), + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for analysis'), + new InputOption('xdebug', mode: InputOption::VALUE_NONE, description: 'Allow running with Xdebug for debugging purposes'), + new InputOption('tmp-file', mode: InputOption::VALUE_REQUIRED, description: '(Editor mode) Edited file used in place of --instead-of file'), + new InputOption('instead-of', mode: InputOption::VALUE_REQUIRED, description: '(Editor mode) File being replaced by --tmp-file'), + new InputOption('fix', mode: InputOption::VALUE_NONE, description: 'Fix auto-fixable errors (experimental)'), + new InputOption('watch', mode: InputOption::VALUE_NONE, description: 'Launch PHPStan Pro'), + new InputOption('pro', mode: InputOption::VALUE_NONE, description: 'Launch PHPStan Pro'), + new InputOption('fail-without-result-cache', mode: InputOption::VALUE_NONE, description: 'Return non-zero exit code when result cache is not used'), ]); } /** * @return string[] */ + #[Override] public function getAliases(): array { return ['analyze']; } + #[Override] protected function initialize(InputInterface $input, OutputInterface $output): void { if ((bool) $input->getOption('debug')) { $application = $this->getApplication(); if ($application === null) { - throw new \PHPStan\ShouldNotHappenException(); + return; } $application->setCatchExceptions(false); return; } } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $paths = $input->getArgument('paths'); @@ -91,10 +142,11 @@ 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'); + $pro = (bool) $input->getOption('watch') || (bool) $input->getOption('pro'); + $fix = (bool) $input->getOption('fix'); + $failWithoutResultCache = (bool) $input->getOption('fail-without-result-cache'); /** @var string|false|null $generateBaselineFile */ $generateBaselineFile = $input->getOption('generate-baseline'); @@ -104,16 +156,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile = 'phpstan-baseline.neon'; } + $allowEmptyBaseline = (bool) $input->getOption('allow-empty-baseline'); + + $tmpFile = $input->getOption('tmp-file'); + $insteadOfFile = $input->getOption('instead-of'); + 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_string($tmpFile) && $tmpFile !== null) + || (!is_string($insteadOfFile) && $insteadOfFile !== null) || (!is_bool($allowXdebug)) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { @@ -121,7 +179,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -129,43 +186,57 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile, $level, $allowXdebug, + $debugEnabled, + $tmpFile, + $insteadOfFile, true, - $debugEnabled ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } - $errorOutput = $inceptionResult->getErrorOutput(); - $obsoleteDockerImage = $_SERVER['PHPSTAN_OBSOLETE_DOCKER_IMAGE'] ?? 'false'; - if ($obsoleteDockerImage === 'true') { - $errorOutput->writeLineFormatted('⚠️ You\'re using an obsolete PHPStan Docker image. ⚠️️'); - $errorOutput->writeLineFormatted(' You can obtain the current one from ghcr.io/phpstan/phpstan.'); - $errorOutput->writeLineFormatted(' Read more about it here:'); - $errorOutput->writeLineFormatted(' https://phpstan.org/user-guide/docker'); - $errorOutput->writeLineFormatted(''); + 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); } + if ($inceptionResult->getEditorModeTmpFile() !== null) { + if ($generateBaselineFile !== null) { + $inceptionResult->getStdOutput()->getStyle()->error('Editor mode options --tmp-file and --instead-of cannot be used when generating the baseline.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + if ($pro) { + $inceptionResult->getStdOutput()->getStyle()->error('Editor mode options --tmp-file and --instead-of cannot be used with PHPStan Pro.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + if ($fix) { + $inceptionResult->getStdOutput()->getStyle()->error('Editor mode options --tmp-file and --instead-of cannot be used with --fix.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + } + + if ($fix) { + if ($generateBaselineFile !== null) { + $inceptionResult->getStdOutput()->getStyle()->error('Errors cannot be fixed when generating the baseline.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + $inceptionResult->getErrorOutput()->getStyle()->note('The --fix CLI option no longer launches PHPStan Pro. Use --pro instead if you want to launch PHPStan Pro'); + } + + $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'; - $ciDetector = new CiDetector(); - - try { - $ci = $ciDetector->detect(); - if ($ci->getCiName() === CiDetector::CI_GITHUB_ACTIONS) { - $errorFormat = 'github'; - } elseif ($ci->getCiName() === CiDetector::CI_TEAMCITY) { - $errorFormat = 'teamcity'; - } - } catch (\OndraM\CiDetector\Exception\CiNotDetectedException $e) { - // pass - } } $container = $inceptionResult->getContainer(); @@ -174,9 +245,7 @@ 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; } @@ -186,29 +255,88 @@ protected function execute(InputInterface $input, OutputInterface $output): int $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); if ($baselineExtension === '') { $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename must have an extension, %s provided instead.', pathinfo($generateBaselineFile, PATHINFO_BASENAME))); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } - if ($baselineExtension !== 'neon') { - $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon, .%s was used instead.', $baselineExtension)); + if (!in_array($baselineExtension, ['neon', 'php'], true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon or .php, .%s was used instead.', $baselineExtension)); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } } try { [$files, $onlyFiles] = $inceptionResult->getFiles(); - } catch (\PHPStan\File\PathNotFoundException $e) { + } catch (PathNotFoundException $e) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + return 1; } - /** @var AnalyseApplication $application */ + if (count($files) === 0) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + + $inceptionResult->getErrorOutput()->getStyle()->error('No files found to analyse.'); + + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + if ($inceptionResult->getEditorModeInsteadOfFile() !== null) { + if (!in_array($inceptionResult->getEditorModeInsteadOfFile(), $files, true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('File %s passed to --instead-of is not in analysed project files.', $inceptionResult->getEditorModeInsteadOfFile())); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + } + + if ($inceptionResult->getEditorModeTmpFile() !== null) { + if (in_array($inceptionResult->getEditorModeTmpFile(), $files, true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('File %s passed to --tmp-file is already in analysed project files.', $inceptionResult->getEditorModeInsteadOfFile())); + 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 ($pro) { + if ($generateBaselineFile !== null) { + $inceptionResult->getStdOutput()->getStyle()->error('You cannot pass the --generate-baseline option when running PHPStan Pro.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); + } + + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); if (!is_bool($debug)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + if ($fix) { + $inceptionResult->getErrorOutput()->writeLineFormatted('Analysing files...'); } try { @@ -221,193 +349,293 @@ protected function execute(InputInterface $input, OutputInterface $output): int $debug, $inceptionResult->getProjectConfigFile(), $inceptionResult->getProjectConfigArray(), - $input + $inceptionResult->getEditorModeTmpFile(), + $inceptionResult->getEditorModeInsteadOfFile(), + $input, ); - } catch (\Throwable $t) { + } catch (Throwable $t) { if ($debug) { - $inceptionResult->getStdOutput()->writeRaw(sprintf( + $stdOutput = $inceptionResult->getStdOutput(); + $stdOutput->writeRaw(sprintf( 'Uncaught %s: %s in %s:%d', get_class($t), $t->getMessage(), $t->getFile(), - $t->getLine() + $t->getLine(), )); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - $inceptionResult->getStdOutput()->writeRaw($t->getTraceAsString()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); + $stdOutput->writeLineFormatted(''); + $stdOutput->writeRaw($t->getTraceAsString()); + $stdOutput->writeLineFormatted(''); + + $previous = $t->getPrevious(); + while ($previous !== null) { + $stdOutput->writeLineFormatted(''); + $stdOutput->writeLineFormatted('Caused by:'); + $stdOutput->writeRaw(sprintf( + 'Uncaught %s: %s in %s:%d', + get_class($previous), + $previous->getMessage(), + $previous->getFile(), + $previous->getLine(), + )); + $stdOutput->writeRaw($previous->getTraceAsString()); + $stdOutput->writeLineFormatted(''); + $previous = $previous->getPrevious(); + } - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } throw $t; } - if ($generateBaselineFile !== null) { - if (!$analysisResult->hasErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + /** + * Variable $internalErrorsTuples contains both "internal errors" + * and "errors with non-ignorable exception" as InternalError objects. + */ + $internalErrorsTuples = []; + $internalFileSpecificErrors = []; + foreach ($analysisResult->getInternalErrorObjects() as $internalError) { + $internalErrorsTuples[$internalError->getMessage()] = [new InternalError( + $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ), false]; + } + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } - return $inceptionResult->handleReturn(1); + $message = $fileSpecificError->getMessage(); + $metadata = $fileSpecificError->getMetadata(); + $hasStackTrace = false; + if ( + $fileSpecificError->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); + $hasStackTrace = true; } - if ($analysisResult->hasInternalErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what\'s going on.'); - return $inceptionResult->handleReturn(1); + if (!$hasStackTrace) { + if (!array_key_exists($fileSpecificError->getMessage(), $internalFileSpecificErrors)) { + $internalFileSpecificErrors[$fileSpecificError->getMessage()] = $fileSpecificError; + } } - $baselineFileDirectory = dirname($generateBaselineFile); - $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + $internalErrorsTuples[$fileSpecificError->getMessage()] = [new InternalError( + $message, + sprintf('analysing file %s', $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ), !$hasStackTrace]; + } - $streamOutput = $this->createStreamOutput(); - $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); - $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + $internalErrorsTuples = array_values($internalErrorsTuples); + + $fileHelper = $container->getByType(FileHelper::class); - $stream = $streamOutput->getStream(); - rewind($stream); - $baselineContents = stream_get_contents($stream); - if ($baselineContents === false) { - throw new \PHPStan\ShouldNotHappenException(); + /** + * Variable $internalErrors only contains non-file-specific "internal errors". + */ + $internalErrors = []; + foreach ($internalErrorsTuples as [$internalError, $isInFileSpecificErrors]) { + if ($isInFileSpecificErrors) { + continue; } - if (!is_dir($baselineFileDirectory)) { - $mkdirResult = @mkdir($baselineFileDirectory, 0644, true); - if ($mkdirResult === false) { - $inceptionResult->getStdOutput()->writeLineFormatted(sprintf('Failed to create directory "%s".', $baselineFileDirectory)); + $internalErrors[] = new InternalError( + $this->getMessageFromInternalError($fileHelper, $internalError, $output->getVerbosity()), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } - return $inceptionResult->handleReturn(1); + if ($generateBaselineFile !== null) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + if (count($internalErrorsTuples) > 0) { + foreach ($internalErrorsTuples as [$internalError]) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); } - } - try { - FileWriter::write($generateBaselineFile, $baselineContents); - } catch (\PHPStan\File\CouldNotWriteFileException $e) { - $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } - $errorsCount = 0; - $unignorableCount = 0; - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->canBeIgnored()) { - $unignorableCount++; - if ($output->isVeryVerbose()) { - $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable could not be added to the baseline:'); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - } - continue; - } + return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache); + } - $errorsCount++; - } + /** @var ErrorFormatter $errorFormatter */ + $errorFormatter = $container->getService($errorFormatterServiceName); - $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); + 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(), + ); - if ( - $unignorableCount === 0 - && count($analysisResult->getNotFileSpecificErrors()) === 0 - ) { - $inceptionResult->getStdOutput()->getStyle()->success($message); - } else { - $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); - } + $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(0); + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, + ); } if ($fix) { - $ciDetector = new CiDetector(); - if ($ciDetector->isCiDetected()) { - $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); - - return $inceptionResult->handleReturn(1); - } - $container->getByType(ResultCacheClearer::class)->clearTemporaryCaches(); - $hasInternalErrors = $analysisResult->hasInternalErrors(); - $nonIgnorableErrorsByException = []; + $fixableErrors = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->hasNonIgnorableException()) { + if ($fileSpecificError->getFixedErrorDiff() === null) { continue; } - $nonIgnorableErrorsByException[] = $fileSpecificError; + $fixableErrors[] = $fileSpecificError; } - if ($hasInternalErrors || count($nonIgnorableErrorsByException) > 0) { - $fixerAnalysisResult = new AnalysisResult( - $nonIgnorableErrorsByException, - $analysisResult->getInternalErrors(), - $analysisResult->getInternalErrors(), - [], - $analysisResult->isDefaultLevelUsed(), - $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved() - ); + $fixableErrorsCount = count($fixableErrors); + if ($fixableErrorsCount === 0) { + $inceptionResult->getStdOutput()->getStyle()->error('No fixable errors found'); + $exitCode = 1; + } else { + $skippedCount = 0; + $diffsByFile = []; + foreach ($fixableErrors as $fixableError) { + $fixFile = $fixableError->getFilePath(); + if ($fixableError->getTraitFilePath() !== null) { + $fixFile = $fixableError->getTraitFilePath(); + } - $stdOutput = $inceptionResult->getStdOutput(); - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + if ($fixableError->getFixedErrorDiff() === null) { + throw new ShouldNotHappenException(); + } - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($fixerAnalysisResult, $stdOutput); + $diffsByFile[$fixFile][] = $fixableError->getFixedErrorDiff(); + } - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + $inceptionResult->getErrorOutput()->writeLineFormatted('Fixing errors...'); + $errorOutput->getStyle()->progressStart($fixableErrorsCount); + + $patcher = $container->getByType(Patcher::class); + foreach ($diffsByFile as $file => $diffs) { + $diffsCount = count($diffs); + try { + $finalFileContents = $patcher->applyDiffs($file, $diffs); + $errorOutput->getStyle()->progressAdvance($diffsCount); + } catch (FileChangedException | MergeConflictException) { + $skippedCount += $diffsCount; + $errorOutput->getStyle()->progressAdvance($diffsCount); + continue; + } - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted(sprintf('hasInternalErrors: %s', $hasInternalErrors ? 'true' : 'false')); - $stdOutput->writeLineFormatted(sprintf('nonIgnorableErrorsByExceptionCount: %d', count($nonIgnorableErrorsByException))); + FileWriter::write($file, $finalFileContents); } - return $inceptionResult->handleReturn(1); + $errorOutput->getStyle()->progressFinish(); + + if ($skippedCount > 0) { + $inceptionResult->getStdOutput()->getStyle()->warning(sprintf( + '%d %s fixed, %d %s skipped', + $fixableErrorsCount, + $fixableErrorsCount === 1 ? 'error' : 'errors', + $skippedCount, + $skippedCount === 1 ? 'error' : 'errors', + )); + } else { + $inceptionResult->getStdOutput()->getStyle()->success(sprintf( + '%d %s fixed', + $fixableErrorsCount, + $fixableErrorsCount === 1 ? 'error' : 'errors', + )); + } + $exitCode = 0; } + } else { + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + } - if (!$analysisResult->isResultCacheSaved() && !$onlyFiles) { - // this can happen only if there are some regex-related errors in ignoreErrors configuration - $stdOutput = $inceptionResult->getStdOutput(); - if (count($analysisResult->getFileSpecificErrors()) > 0) { - $stdOutput->getStyle()->error('Unknown error. Please report this as a bug.'); - return $inceptionResult->handleReturn(1); - } + if ($exitCode === 0 && $failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + 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)); + } - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($analysisResult, $stdOutput); + $errorOutput->writeLineFormatted(''); - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted('Result cache was not saved.'); + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); } - return $inceptionResult->handleReturn(1); - } + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; - $inceptionResult->handleReturn(0); // delete memory limit file + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); - /** @var FixerApplication $fixerApplication */ - $fixerApplication = $container->getByType(FixerApplication::class); + $errorOutput->writeLineFormatted(''); - return $fixerApplication->run( - $inceptionResult->getProjectConfigFile(), - $inceptionResult, - $input, - $output, - $analysisResult->getFileSpecificErrors(), - $analysisResult->getNotFileSpecificErrors(), - count($files), - $_SERVER['argv'][0] - ); + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); + } + + $errorOutput->writeLineFormatted(''); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } } - /** @var ErrorFormatter $errorFormatter */ - $errorFormatter = $container->getService($errorFormatterServiceName); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()) + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, ); } @@ -415,9 +643,207 @@ private function createStreamOutput(): StreamOutput { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new StreamOutput($resource); } + private function getMessageFromInternalError(FileHelper $fileHelper, InternalError $internalError, int $verbosity): string + { + $message = sprintf('%s while %s', $internalError->getMessage(), $internalError->getContextDescription()); + $hasLarastan = false; + $isLaravelLast = false; + + foreach (array_reverse($internalError->getTrace()) as $traceItem) { + if ($traceItem['file'] === null) { + continue; + } + + $file = $fileHelper->normalizePath($traceItem['file'], '/'); + + if (str_contains($file, '/larastan/')) { + $hasLarastan = true; + $isLaravelLast = false; + continue; + } + + if (!str_contains($file, '/laravel/framework/')) { + continue; + } + + $isLaravelLast = true; + } + if ($hasLarastan) { + if ($isLaravelLast) { + $message .= "\n"; + $message .= "\n" . 'This message is coming from Laravel Framework itself.'; + $message .= "\n" . 'Larastan boots up your application in order to provide'; + $message .= "\n" . 'smarter static analysis of your codebase.'; + $message .= "\n"; + $message .= "\n" . 'In order to do that, the environment you run PHPStan in'; + $message .= "\n" . 'must match the environment you run your application in.'; + $message .= "\n"; + $message .= "\n" . 'Make sure you\'ve set your environment variables'; + $message .= "\n" . 'or the .env file correctly.'; + + return $message; + } + + $bugReportUrl = 'https://github.com/larastan/larastan/issues/new?template=bug-report.md'; + } else { + $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; + } + if ($internalError->getTraceAsString() !== null) { + if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) { + $firstTraceItem = $internalError->getTrace()[0] ?? null; + $trace = ''; + if ($firstTraceItem !== null && $firstTraceItem['file'] !== null && $firstTraceItem['line'] !== null) { + $trace = sprintf('## %s(%d)%s', $firstTraceItem['file'], $firstTraceItem['line'], "\n"); + } + $trace .= $internalError->getTraceAsString(); + + 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"); + } + } + } + + return $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); + $rawMessageInBaseline = $inceptionResult->getContainer()->getParameter('featureToggles')['rawMessageInBaseline']; + + if ($baselineExtension === 'php') { + $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper, $rawMessageInBaseline); + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + } else { + $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper, $rawMessageInBaseline); + $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + } + + $stream = $streamOutput->getStream(); + rewind($stream); + $baselineContents = stream_get_contents($stream); + + 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 with \"-vv\" and fix them."); + } + } + + $exitCode = 0; + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } + + 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 runDiagnoseExtensions(Container $container, Output $errorOutput): void + { + 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); + } + } + } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index e52904d869..f6ea231e84 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -2,81 +2,85 @@ namespace PHPStan\Command; +use Closure; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResult; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Parallel\ParallelAnalyser; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; +use PHPStan\ShouldNotHappenException; +use React\EventLoop\StreamSelectLoop; use Symfony\Component\Console\Input\InputInterface; +use function array_filter; +use function array_unshift; +use function array_values; +use function count; +use function function_exists; +use function is_file; +use function memory_get_peak_usage; -class AnalyserRunner +#[AutowiredService] +final class AnalyserRunner { - private Scheduler $scheduler; - - private Analyser $analyser; - - private ParallelAnalyser $parallelAnalyser; - - private CpuCoreCounter $cpuCoreCounter; - public function __construct( - Scheduler $scheduler, - Analyser $analyser, - ParallelAnalyser $parallelAnalyser, - CpuCoreCounter $cpuCoreCounter + private Scheduler $scheduler, + private Analyser $analyser, + private ParallelAnalyser $parallelAnalyser, + private CpuCoreCounter $cpuCoreCounter, ) { - $this->scheduler = $scheduler; - $this->analyser = $analyser; - $this->parallelAnalyser = $parallelAnalyser; - $this->cpuCoreCounter = $cpuCoreCounter; } /** * @param string[] $files * @param string[] $allAnalysedFiles - * @param (\Closure(string $file): void)|null $preFileCallback - * @param (\Closure(int): void)|null $postFileCallback - * @param bool $debug - * @param bool $allowParallel - * @param string|null $projectConfigFile - * @param string|null $tmpFile - * @param string|null $insteadOfFile - * @param InputInterface $input - * @return AnalyserResult + * @param Closure(string $file): void|null $preFileCallback + * @param Closure(int ): void|null $postFileCallback */ public function runAnalyser( array $files, array $allAnalysedFiles, - ?\Closure $preFileCallback, - ?\Closure $postFileCallback, + ?Closure $preFileCallback, + ?Closure $postFileCallback, bool $debug, bool $allowParallel, ?string $projectConfigFile, ?string $tmpFile, ?string $insteadOfFile, - InputInterface $input + InputInterface $input, ): AnalyserResult { $filesCount = count($files); if ($filesCount === 0) { - return new AnalyserResult([], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); $mainScript = null; - if (isset($_SERVER['argv'][0]) && file_exists($_SERVER['argv'][0])) { + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { $mainScript = $_SERVER['argv'][0]; } if ( !$debug && $allowParallel + && function_exists('proc_open') && $mainScript !== null - && $schedule->getNumberOfProcesses() > 1 + && $schedule->getNumberOfProcesses() > 0 ) { - return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input); + $loop = new StreamSelectLoop(); + $result = null; + $promise = $this->parallelAnalyser->analyse($loop, $schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $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( @@ -84,30 +88,27 @@ public function runAnalyser( $preFileCallback, $postFileCallback, $debug, - $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile) + $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), ); } /** * @param string[] $analysedFiles - * @param string|null $insteadOfFile - * @param string|null $tmpFile * @return string[] */ private function switchTmpFile( array $analysedFiles, ?string $insteadOfFile, - ?string $tmpFile + ?string $tmpFile, ): array { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); + if ($insteadOfFile === null) { + return $analysedFiles; + } + $analysedFiles = array_values(array_filter($analysedFiles, static fn (string $file): bool => $file !== $insteadOfFile)); + if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; + array_unshift($analysedFiles, $tmpFile); } return $analysedFiles; diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index 2398b0833c..1697b5f38a 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -3,69 +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 $internalErrors; - - /** @var string[] */ - private array $warnings; - - private bool $defaultLevelUsed; - - private ?string $projectConfigFile; - - private bool $savedResultCache; - /** - * @param \PHPStan\Analyser\Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - * @param string[] $internalErrors - * @param string[] $warnings - * @param bool $defaultLevelUsed - * @param string|null $projectConfigFile - * @param bool $savedResultCache + * @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 $internalErrors, - array $warnings, - bool $defaultLevelUsed, - ?string $projectConfigFile, - bool $savedResultCache + 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->internalErrors = $internalErrors; - $this->warnings = $warnings; - $this->defaultLevelUsed = $defaultLevelUsed; - $this->projectConfigFile = $projectConfigFile; - $this->savedResultCache = $savedResultCache; } public function hasErrors(): bool @@ -79,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 { @@ -87,7 +74,7 @@ public function getFileSpecificErrors(): array } /** - * @return string[] + * @return list */ public function getNotFileSpecificErrors(): array { @@ -95,15 +82,15 @@ public function getNotFileSpecificErrors(): array } /** - * @return string[] + * @return list */ - public function getInternalErrors(): array + public function getInternalErrorObjects(): array { return $this->internalErrors; } /** - * @return string[] + * @return list */ public function getWarnings(): array { @@ -115,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; @@ -135,4 +130,22 @@ public function isResultCacheSaved(): bool return $this->savedResultCache; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + + public function isResultCacheUsed(): bool + { + return $this->isResultCacheUsed; + } + + /** + * @return array + */ + public function getChangedProjectExtensionFilesOutsideOfAnalysedPaths(): array + { + return $this->changedProjectExtensionFilesOutsideOfAnalysedPaths; + } + } diff --git a/src/Command/ClearResultCacheCommand.php b/src/Command/ClearResultCacheCommand.php index 6b5198c4dc..1ca5f60b6e 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -2,31 +2,32 @@ namespace PHPStan\Command; +use Override; 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; } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -34,43 +35,66 @@ 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', mode: InputOption::VALUE_NONE, description: 'Show debug information - which file is analysed, do not catch internal errors'), + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for clearing result cache'), + new InputOption('xdebug', mode: InputOption::VALUE_NONE, description: 'Allow running with Xdebug for debugging purposes'), ]); } + #[Override] + 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; + } + } + + #[Override] 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, + null, + null, + true, ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { return 1; } $container = $inceptionResult->getContainer(); - /** @var ResultCacheClearer $resultCacheClearer */ $resultCacheClearer = $container->getByType(ResultCacheClearer::class); $path = $resultCacheClearer->clear(); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index c21d8d1843..a599a83efa 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,42 +2,88 @@ 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\DependencyInjection\MissingImplementedInterfaceInServiceWithTagException; +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 \PHPStan\Command\InceptionNotSuccessfulException + * @throws InceptionNotSuccessfulException */ public static function begin( InputInterface $input, OutputInterface $output, array $paths, - ?string $pathsFile, ?string $memoryLimit, ?string $autoloadFile, array $composerAutoloaderProjectPaths, @@ -45,46 +91,91 @@ public static function begin( ?string $generateBaselineFile, ?string $level, bool $allowXdebug, - bool $manageMemoryLimitFile = true, - bool $debugEnabled = false, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null + bool $debugEnabled, + ?string $singleReflectionFile, + ?string $singleReflectionInsteadOfFile, + bool $cleanupContainerCache, ): InceptionResult { - if (!$allowXdebug) { - $xdebug = new XdebugHandler('phpstan'); - $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 { @@ -92,7 +183,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; @@ -108,65 +207,73 @@ public static function begin( $generateBaselineFile = $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($generateBaselineFile)); } - $defaultLevelUsed = false; - if ($projectConfigFile === null && $level === null) { - $level = self::DEFAULT_LEVEL; - $defaultLevelUsed = true; - } - - $paths = array_map(static function (string $path) use ($currentWorkingDirectoryFileHelper): string { - return $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)); - }, $paths); + if ($singleReflectionFile !== null) { + $singleReflectionFile = $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($singleReflectionFile)); + if (!is_file($singleReflectionFile)) { + $errorOutput->writeLineFormatted(sprintf('File passed to --tmp-file option does not exist: %s', $singleReflectionFile)); + throw new InceptionNotSuccessfulException(); + } - 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(); + if ($singleReflectionInsteadOfFile === null) { + $errorOutput->writeLineFormatted('Both --tmp-file and --instead-of options must be passed at the same time for editor mode to work.'); + throw new InceptionNotSuccessfulException(); } + } - try { - $pathsString = FileReader::read($pathsFile); - } catch (\PHPStan\File\CouldNotReadFileException $e) { - $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + if ($singleReflectionInsteadOfFile !== null) { + $singleReflectionInsteadOfFile = $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($singleReflectionInsteadOfFile)); + if (!is_file($singleReflectionInsteadOfFile)) { + $errorOutput->writeLineFormatted(sprintf('File passed to --instead-of option does not exist: %s', $singleReflectionInsteadOfFile)); + throw new InceptionNotSuccessfulException(); } - $paths = array_values(array_filter(explode("\n", $pathsString), static function (string $path): bool { - return trim($path) !== ''; - })); + if ($singleReflectionFile === null) { + $errorOutput->writeLineFormatted('Both --tmp-file and --instead-of options must be passed at the same time for editor mode to work.'); + throw new InceptionNotSuccessfulException(); + } + } - $pathsFileFileHelper = new FileHelper(dirname($pathsFile)); - $paths = array_map(static function (string $path) use ($pathsFileFileHelper): string { - return $pathsFileFileHelper->normalizePath($pathsFileFileHelper->absolutizePath($path)); - }, $paths); + $defaultLevelUsed = false; + if ($projectConfigFile === null && $level === null) { + $level = self::DEFAULT_LEVEL; + $defaultLevelUsed = true; } + $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, + [ + '[parameters][paths][]', + '[parameters][tmpDir]', + ], ))->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'])) { @@ -175,8 +282,10 @@ public static function begin( if ($level === null && isset($projectConfig['parameters']['level'])) { $level = (string) $projectConfig['parameters']['level']; } - if (count($paths) === 0 && isset($projectConfig['parameters']['paths'])) { + if (isset($projectConfig['parameters']['paths'])) { $analysedPathsFromConfig = Helpers::expand($projectConfig['parameters']['paths'], $defaultParameters); + } + if (count($paths) === 0) { $paths = $analysedPathsFromConfig; } } @@ -186,25 +295,25 @@ public static function begin( $levelConfigFile = sprintf('%s/config.level%s.neon', $containerFactory->getConfigDirectory(), $level); if (!is_file($levelConfigFile)) { $errorOutput->writeLineFormatted(sprintf('Level config file %s was not found.', $levelConfigFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $additionalConfigFiles[] = $levelConfigFile; } if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { - $generatedConfigReflection = new \ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); + $generatedConfigReflection = new ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); $generatedConfigDirectory = dirname($generatedConfigReflection->getFileName()); - foreach (\PHPStan\ExtensionInstaller\GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { + foreach (GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { foreach ($extensionConfig['extra']['includes'] ?? [] as $includedFile) { if (!is_string($includedFile)) { $errorOutput->writeLineFormatted(sprintf('Cannot include config from package %s, expecting string file path but got %s', $name, gettype($includedFile))); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $includedFilePath = null; if (isset($extensionConfig['relative_install_path'])) { $includedFilePath = sprintf('%s/%s/%s', $generatedConfigDirectory, $extensionConfig['relative_install_path'], $includedFile); - if (!file_exists($includedFilePath) || !is_readable($includedFilePath)) { + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { $includedFilePath = null; } } @@ -212,13 +321,50 @@ public static function begin( if ($includedFilePath === null) { $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); } - if (!file_exists($includedFilePath) || !is_readable($includedFilePath)) { + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { $errorOutput->writeLineFormatted(sprintf('Config file %s does not exist or isn\'t readable', $includedFilePath)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $additionalConfigFiles[] = $includedFilePath; } } + + if ( + 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 ( @@ -228,22 +374,12 @@ public static function begin( $additionalConfigFiles[] = $projectConfigFile; } - $loaderParameters = [ - 'rootDir' => $containerFactory->getRootDirectory(), - 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), - ]; - - self::detectDuplicateIncludedFiles( - $errorOutput, - $currentWorkingDirectoryFileHelper, - $additionalConfigFiles, - $loaderParameters - ); - $createDir = static function (string $path) use ($errorOutput): void { - if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) { - $errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + try { + DirectoryCreator::ensureDirectoryExists($path, 0777); + } catch (DirectoryCreatorException $e) { + $errorOutput->writeLineFormatted($e->getMessage()); + throw new InceptionNotSuccessfulException(); } }; @@ -254,32 +390,123 @@ public static function begin( try { $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile, $singleReflectionFile, $singleReflectionInsteadOfFile); - } catch (\Nette\DI\InvalidConfigurationException | \Nette\Utils\AssertionException $e) { + } catch (InvalidConfigurationException | AssertionException $e) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + throw new InceptionNotSuccessfulException(); + } catch (InvalidIgnoredErrorPatternsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in ignoreErrors:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } - $containerFactory->clearOldContainers($tmpDir); + $errorOutput->writeLineFormatted('To ignore non-existent paths in ignoreErrors,'); + $errorOutput->writeLineFormatted('set reportUnmatchedIgnoredErrors: false in your configuration file.'); + $errorOutput->writeLineFormatted(''); - if (count($paths) === 0) { - $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + throw new InceptionNotSuccessfulException(); + } catch (MissingImplementedInterfaceInServiceWithTagException $e) { + $errorOutput->writeLineFormatted('Invalid service:'); + $errorOutput->writeLineFormatted($e->getMessage()); + $errorOutput->writeLineFormatted(''); + + throw new InceptionNotSuccessfulException(); + } catch (InvalidExcludePathsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in excludePaths:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } + + $suggestOptional = $e->getSuggestOptional(); + if (count($suggestOptional) > 0) { + $baselinePathHelper = null; + if ($projectConfigFile !== null) { + $baselinePathHelper = new ParentDirectoryRelativePathHelper(dirname($projectConfigFile)); + } + $errorOutput->writeLineFormatted('If the excluded path can sometimes exist, append (?)'); + $errorOutput->writeLineFormatted('to its config entry to mark it as optional. Example:'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('parameters:'); + $errorOutput->writeLineFormatted("\texcludePaths:"); + foreach ($suggestOptional as $key => $suggestOptionalPaths) { + $errorOutput->writeLineFormatted(sprintf("\t\t%s:", $key)); + foreach ($suggestOptionalPaths as $suggestOptionalPath) { + if ($baselinePathHelper === null) { + $errorOutput->writeLineFormatted(sprintf("\t\t\t- %s (?)", $suggestOptionalPath)); + continue; + } + + $errorOutput->writeLineFormatted(sprintf("\t\t\t- %s (?)", $baselinePathHelper->getRelativePath($suggestOptionalPath))); + } + } + $errorOutput->writeLineFormatted(''); + } + + throw new InceptionNotSuccessfulException(); + } catch (ValidationException $e) { + foreach ($e->getMessages() as $message) { + $errorOutput->writeLineFormatted('Invalid configuration:'); + $errorOutput->writeLineFormatted($message); + } + throw new InceptionNotSuccessfulException(); + } catch (ServiceCreationException $e) { + $matches = Strings::match($e->getMessage(), '#Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]): Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]) needed by \$(?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*) in (?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)\(\)#'); + if ($matches === null) { + throw $e; + } + + if ($matches['parserServiceType'] !== 'PHPStan\\Parser\\Parser') { + throw $e; + } - $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)); + 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)'); + + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('After fixing this problem, your configuration will look something like this:'); $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('-'); + $errorOutput->writeLineFormatted(sprintf("\tclass: %s", $matches['serviceType'])); + $errorOutput->writeLineFormatted(sprintf("\targuments:")); + $errorOutput->writeLineFormatted(sprintf("\t\t%s: @defaultAnalysisParser", $matches['parameterName'])); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('To avoid this issue, allow to use more memory with the --memory-limit option.'); - @unlink($memoryLimitFile); + + throw new InceptionNotSuccessfulException(); + } 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(); + } } - self::setUpSignalHandler($errorOutput, $manageMemoryLimitFile ? $memoryLimitFile : null); - if (!$container->hasParameter('customRulesetUsed')) { + /** @var bool|null $customRulesetUsed */ + $customRulesetUsed = $container->getParameter('customRulesetUsed'); + if ($customRulesetUsed === null) { $errorOutput->writeLineFormatted(''); $errorOutput->writeLineFormatted('No rules detected'); $errorOutput->writeLineFormatted(''); @@ -290,29 +517,39 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('* create your own custom ruleset by selecting which rules you want to check by copying the service definitions from the built-in config level files in %s.', $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf'))); $errorOutput->writeLineFormatted(' * in this case, don\'t forget to define parameter customRulesetUsed in your config file.'); $errorOutput->writeLineFormatted(''); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } elseif ((bool) $container->getParameter('customRulesetUsed')) { + throw new InceptionNotSuccessfulException(); + } elseif ($customRulesetUsed) { $defaultLevelUsed = false; } - $schema = $container->getParameter('__parametersSchema'); - $processor = new Processor(); - $processor->onNewContext[] = static function (SchemaContext $context): void { - $context->path = ['parameters']; - }; + foreach ($container->getParameter('bootstrapFiles') as $bootstrapFileFromArray) { + self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); + } - try { - $processor->process($schema, $container->getParameters()); - } catch (\Nette\Schema\ValidationException $e) { - foreach ($e->getMessages() as $message) { - $errorOutput->writeLineFormatted('Invalid configuration:'); - $errorOutput->writeLineFormatted($message); + /** @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; } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + + $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) { @@ -322,7 +559,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) { @@ -332,7 +569,7 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Scanned directory %s does not exist.', $scannedDirectory)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $alreadyAddedStubFiles = []; @@ -340,7 +577,7 @@ public static function begin( if (array_key_exists($stubFile, $alreadyAddedStubFiles)) { $errorOutput->writeLineFormatted(sprintf('Stub file %s is added multiple times.', $stubFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $alreadyAddedStubFiles[$stubFile] = true; @@ -351,35 +588,31 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Stub file %s does not exist.', $stubFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } - $excludesAnalyse = $container->getParameter('excludes_analyse'); - $excludePaths = $container->getParameter('excludePaths'); - if (count($excludesAnalyse) > 0 && $excludePaths !== null) { - $errorOutput->writeLineFormatted(sprintf('Configuration parameters excludes_analyse and excludePaths cannot be used at the same time.')); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); - $errorOutput->writeLineFormatted(''); - - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } elseif (count($excludesAnalyse) > 0) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option excludes_analyse. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); - } - - $tempResultCachePath = $container->getParameter('tempResultCachePath'); - $createDir($tempResultCachePath); - /** @var FileFinder $fileFinder */ $fileFinder = $container->getService('fileFinderAnalyse'); - /** @var \Closure(): (array{string[], bool}) $filesCallback */ - $filesCallback = static function () use ($fileFinder, $paths): array { + $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); + $files = $fileFinderResult->getFiles(); + + $pathRoutingParser->setAnalysedFiles($files); - return [$fileFinderResult->getFiles(), $fileFinderResult->isOnlyFiles()]; + $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( @@ -388,10 +621,11 @@ public static function begin( $errorOutput, $container, $defaultLevelUsed, - $memoryLimitFile, $projectConfigFile, $projectConfig, - $generateBaselineFile + $generateBaselineFile, + $singleReflectionFile, + $singleReflectionInsteadOfFile, ); } @@ -402,136 +636,26 @@ 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; + throw new InceptionNotSuccessfulException(); } - - 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 \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..03d8d78455 --- /dev/null +++ b/src/Command/DiagnoseCommand.php @@ -0,0 +1,112 @@ +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', mode: InputOption::VALUE_NONE, description: 'Show debug information - do not catch internal errors'), + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for clearing result cache'), + ]); + } + + #[Override] + 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; + } + } + + #[Override] + 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, + null, + null, + false, + ); + } catch (InceptionNotSuccessfulException) { + return 1; + } + + $container = $inceptionResult->getContainer(); + $output = $inceptionResult->getStdOutput(); + + /** @var PHPStanDiagnoseExtension $phpstanDiagnoseExtension */ + $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); + + // not using tag for this extension to make sure it's always first + $phpstanDiagnoseExtension->print($output); + + /** @var DiagnoseExtension $extension */ + foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { + $extension->print($output); + } + + return 0; + } + +} diff --git a/src/Command/DumpParametersCommand.php b/src/Command/DumpParametersCommand.php new file mode 100644 index 0000000000..4ea86d2125 --- /dev/null +++ b/src/Command/DumpParametersCommand.php @@ -0,0 +1,122 @@ +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', mode: InputOption::VALUE_NONE, description: 'Show debug information - which file is analysed, do not catch internal errors'), + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for clearing result cache'), + new InputOption('json', mode: InputOption::VALUE_NONE, description: 'Dump parameters as JSON instead of NEON'), + ]); + } + + #[Override] + 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; + } + } + + #[Override] + 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, + null, + null, + 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']); + + // internal - editor mode + unset($parameters['singleReflectionFile']); + unset($parameters['singleReflectionInsteadOfFile']); + + 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 33a308aa96..b5ea4914f1 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -4,34 +4,32 @@ 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 const SORT_STRING; +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, private bool $useRawMessage) { - $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; } @@ -40,39 +38,95 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); + $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; } ksort($fileErrors, SORT_STRING); + $messageKey = $this->useRawMessage ? 'rawMessage' : 'message'; $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($fileErrorsCounts, SORT_STRING); - - foreach ($fileErrorsCounts as $message => $count) { - $errorsToOutput[] = [ - 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), - 'count' => $count, - 'path' => Helpers::escape($file), - ]; + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + if (!$this->useRawMessage) { + $message = '#^' . preg_quote($message, '#') . '$#'; + } + + ksort($identifiers, SORT_STRING); + if (count($identifiers) > 0) { + foreach ($identifiers as $identifier => $identifierCount) { + $errorsToOutput[] = [ + $messageKey => Helpers::escape($message), + 'identifier' => $identifier, + 'count' => $identifierCount, + 'path' => Helpers::escape($file), + ]; + } + } else { + $errorsToOutput[] = [ + $messageKey => Helpers::escape($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..892a3e1eb2 --- /dev/null +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -0,0 +1,119 @@ +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]) { + if ($this->useRawMessage) { + $messageKey = 'rawMessage'; + } else { + $messageKey = 'message'; + $message = '#^' . preg_quote($message, '#') . '$#'; + } + + ksort($identifiers, SORT_STRING); + if (count($identifiers) > 0) { + foreach ($identifiers as $identifier => $identifierCount) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export($messageKey, true), + var_export(Helpers::escape($message), true), + var_export(Helpers::escape($identifier), true), + var_export($identifierCount, true), + var_export(Helpers::escape($file), true), + ); + } + } else { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export($messageKey, true), + var_export(Helpers::escape($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 b2cbb6ade0..8120dea1ef 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -2,23 +2,32 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; - -class CheckstyleErrorFormatter implements ErrorFormatter +use function count; +use function htmlspecialchars; +use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; + +#[AutowiredService(name: 'errorFormatter.checkstyle')] +final class CheckstyleErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { $output->writeRaw(''); @@ -29,15 +38,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($error->getMessage()) + $this->escape($error->getMessage()), + $error->getIdentifier() !== null ? sprintf(' source="%s"', $this->escape($error->getIdentifier())) : '', )); $output->writeLineFormatted(''); } @@ -82,8 +92,6 @@ public function formatErrors( /** * Escapes values for using in XML * - * @param string $string - * @return string */ private function escape(string $string): string { @@ -93,22 +101,21 @@ private function escape(string $string): string /** * Group errors by file * - * @param AnalysisResult $analysisResult - * @return array> Array that have as key the relative path of file - * and as value an array with occurred errors. + * @return array> Array that have as key the relative path of file + * and as value an array with occurred errors. */ private function groupByFile(AnalysisResult $analysisResult): array { $files = []; - /** @var \PHPStan\Analyser\Error $fileSpecificError */ + /** @var Error $fileSpecificError */ foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $absolutePath = $fileSpecificError->getFilePath(); if ($fileSpecificError->getTraitFilePath() !== null) { $absolutePath = $fileSpecificError->getTraitFilePath(); } $relativeFilePath = $this->relativePathHelper->getRelativePath( - $absolutePath + $absolutePath, ); $files[$relativeFilePath][] = $fileSpecificError; diff --git a/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php new file mode 100644 index 0000000000..403ebdb13f --- /dev/null +++ b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php @@ -0,0 +1,47 @@ +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 3c2663efe7..2d4e17ee31 100644 --- a/src/Command/ErrorFormatter/ErrorFormatter.php +++ b/src/Command/ErrorFormatter/ErrorFormatter.php @@ -5,20 +5,31 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; -/** @api */ +/** + * This is the interface custom error formatters implement. Register it in the configuration file + * like this: + * + * ``` + * services: + * errorFormatter.myFormat: + * class: App\PHPStan\AwesomeErrorFormatter + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/error-formatters + * + * @api + */ interface ErrorFormatter { /** * Formats the errors and outputs them to the console. * - * @param \PHPStan\Command\AnalysisResult $analysisResult - * @param \PHPStan\Command\Output $output * @return int Error code. */ public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int; } diff --git a/src/Command/ErrorFormatter/GithubErrorFormatter.php b/src/Command/ErrorFormatter/GithubErrorFormatter.php index 0743b26d3a..a81fd0bd01 100644 --- a/src/Command/ErrorFormatter/GithubErrorFormatter.php +++ b/src/Command/ErrorFormatter/GithubErrorFormatter.php @@ -4,32 +4,32 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; +use function array_walk; +use function implode; +use function preg_replace; +use function sprintf; +use function str_replace; /** * Allow errors to be reported in pull-requests diff when run in a GitHub Action * @see https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message */ -class GithubErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.github')] +final class GithubErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - private TableErrorFormatter $tableErrorformatter; - public function __construct( - RelativePathHelper $relativePathHelper, - TableErrorFormatter $tableErrorformatter + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, ) { - $this->relativePathHelper = $relativePathHelper; - $this->tableErrorformatter = $tableErrorformatter; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int { - $this->tableErrorformatter->formatErrors($analysisResult, $output); - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $metas = [ 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), @@ -41,9 +41,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in }); $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); + $message = $this->formatMessage($message); $line = sprintf('::error %s::%s', implode(',', $metas), $message); @@ -52,9 +50,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in } 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); + $notFileSpecificError = $this->formatMessage($notFileSpecificError); $line = sprintf('::error ::%s', $notFileSpecificError); @@ -63,8 +59,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in } foreach ($analysisResult->getWarnings() as $warning) { - // newlines need to be encoded - // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $warning = $this->formatMessage($warning); $warning = str_replace("\n", '%0A', $warning); $line = sprintf('::warning ::%s', $warning); @@ -76,4 +71,13 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in return $analysisResult->hasErrors() ? 1 : 0; } + private function formatMessage(string $message): string + { + // newlines need to be encoded + // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $message = str_replace("\n", '%0A', $message); + + return preg_replace('/(^|\s)@([a-zA-Z0-9_\-]+)(\s|$)/', '$1`@$2`$3', $message) ?? $message; + } + } diff --git a/src/Command/ErrorFormatter/GitlabErrorFormatter.php b/src/Command/ErrorFormatter/GitlabErrorFormatter.php index 5bc495d6d2..a5c038320b 100644 --- a/src/Command/ErrorFormatter/GitlabErrorFormatter.php +++ b/src/Command/ErrorFormatter/GitlabErrorFormatter.php @@ -5,19 +5,24 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService(name: 'errorFormatter.gitlab')] +final class GitlabErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int @@ -34,8 +39,8 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $fileSpecificError->getFile(), $fileSpecificError->getLine(), $fileSpecificError->getMessage(), - ] - ) + ], + ), ), 'severity' => $fileSpecificError->canBeIgnored() ? 'major' : 'blocker', 'location' => [ 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 3476afed65..b3d681ec62 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -4,29 +4,38 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; +use function htmlspecialchars; use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; -class JunitErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.junit')] +final class JunitErrorFormatter implements ErrorFormatter { - private \PHPStan\File\RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + 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 +43,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 +69,7 @@ public function formatErrors( /** * Format a single test case * - * @param string $reference - * @param string|null $message * - * @return string */ private function createTestCase(string $reference, string $type, ?string $message = null): string { @@ -81,8 +87,6 @@ private function createTestCase(string $reference, string $type, ?string $messag /** * Escapes values for using in XML * - * @param string $string - * @return string */ private function escape(string $string): string { diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index 7d926c3b7c..a37dc42129 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -4,13 +4,16 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredService; +use function sprintf; -class RawErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.raw')] +final class RawErrorFormatter implements ErrorFormatter { public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { @@ -18,14 +21,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 1ef40d97d1..bf79a7dc5f 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -2,37 +2,53 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalyseCommand; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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; + +#[AutowiredService(name: 'errorFormatter.table')] +final class TableErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - private bool $showTipsOfTheDay; - - private ?string $editorUrl; - public function __construct( - RelativePathHelper $relativePathHelper, - bool $showTipsOfTheDay, - ?string $editorUrl = null + private RelativePathHelper $relativePathHelper, + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private SimpleRelativePathHelper $simpleRelativePathHelper, + private CiDetectedErrorFormatter $ciDetectedErrorFormatter, + #[AutowiredParameter(ref: '%tipsOfTheDay%')] + private bool $showTipsOfTheDay, + #[AutowiredParameter] + private ?string $editorUrl, + #[AutowiredParameter] + private ?string $editorUrlTitle, ) { - $this->relativePathHelper = $relativePathHelper; - $this->showTipsOfTheDay = $showTipsOfTheDay; - $this->editorUrl = $editorUrl; } /** @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()); @@ -42,13 +58,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(''); } @@ -57,7 +74,7 @@ public function formatErrors( return 0; } - /** @var array $fileErrors */ + /** @var array $fileErrors */ $fileErrors = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { if (!isset($fileErrors[$fileSpecificError->getFile()])) { @@ -71,36 +88,74 @@ 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)) { - $message .= "\n✏️ " . str_replace(['%file%', '%line%'], [$error->getTraitFilePath() ?? $error->getFilePath(), (string) $error->getLine()], $this->editorUrl); + + if (getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm') { + $title = $this->relativePathHelper->getRelativePath($filePath); + $message .= sprintf("\nat %s:%d", $title, $error->getLine()); + + } elseif (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()); @@ -117,4 +172,18 @@ public function formatErrors( return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0; } + private function formatLineNumber(?int $lineNumber): string + { + if ($lineNumber === null) { + return ''; + } + + $isRunningInVSCodeTerminal = getenv('TERM_PROGRAM') === 'vscode'; + if ($isRunningInVSCodeTerminal) { + return ':' . $lineNumber; + } + + return (string) $lineNumber; + } + } diff --git a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php index 17bbb080af..3d00164719 100644 --- a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php +++ b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php @@ -4,19 +4,29 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; +use function array_keys; +use function array_values; +use function count; +use function is_string; +use function preg_replace; +use function sprintf; +use const PHP_EOL; /** * @see https://www.jetbrains.com/help/teamcity/build-script-interaction-with-teamcity.html#Reporting+Inspections */ -class TeamcityErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.teamcity')] +final class TeamcityErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int @@ -38,9 +48,15 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in ]); foreach ($fileSpecificErrors as $fileSpecificError) { + $message = $fileSpecificError->getMessage(); + + if ($fileSpecificError->getIdentifier() !== null && $fileSpecificError->canBeIgnored()) { + $message .= sprintf(' (🪪 %s)', $fileSpecificError->getIdentifier()); + } + $result .= $this->createTeamcityLine('inspection', [ 'typeId' => 'phpstan', - 'message' => $fileSpecificError->getMessage(), + 'message' => $message, 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), 'line' => $fileSpecificError->getLine(), // additional attributes diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index f106573a69..18301f5ab8 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -3,18 +3,29 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; +use Override; +use Symfony\Component\Console\Helper\Helper; 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 strlen; +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; @@ -38,13 +49,15 @@ private function isCiDetected(): bool * @param string[] $headers * @param string[][] $rows */ + #[Override] 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]); + $length = Helper::width(Helper::removeDecoration($this->getFormatter(), $row[0])); + if ($maxHeaderWidth !== 0 && $length <= $maxHeaderWidth) { continue; } @@ -52,42 +65,79 @@ public function table(array $headers, array $rows): void $maxHeaderWidth = $length; } - $wrap = static function ($rows) use ($terminalWidth, $maxHeaderWidth): array { - return array_map(static function ($row) use ($terminalWidth, $maxHeaderWidth): array { - return array_map(static function ($s) use ($terminalWidth, $maxHeaderWidth) { - if ($terminalWidth > $maxHeaderWidth + 5) { - return wordwrap( - $s, - $terminalWidth - $maxHeaderWidth - 5, - "\n", - true - ); - } - - return $s; - }, $row); - }, $rows); - }; - - parent::table($headers, $wrap($rows)); + foreach ($headers as $i => $header) { + $newHeader = []; + foreach (explode("\n", $header) as $h) { + $newHeader[] = sprintf('%s', $h); + } + + $headers[$i] = implode("\n", $newHeader); + } + + $table = $this->createTable(); + // -5 because there are 5 padding spaces: One on each side of the table, one on each side of a cell and one between columns. + $table->setColumnMaxWidth(1, $terminalWidth - $maxHeaderWidth - 5); + array_unshift($rows, $headers, new TableSeparator()); + $table->setRows($rows); + + $table->render(); + $this->newLine(); } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $max - */ - public function createProgressBar($max = 0): ProgressBar + #[Override] + 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); + } + + #[Override] + public function progressStart(int $max = 0): void { if (!$this->showProgress) { return; @@ -95,28 +145,17 @@ public function progressStart($max = 0): void parent::progressStart($max); } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $step - */ - public function progressAdvance($step = 1): void + #[Override] + 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); } + #[Override] public function progressFinish(): void { if (!$this->showProgress) { diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index 957dc82bb0..0ee0689ede 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -5,254 +5,208 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use Composer\CaBundle\CaBundle; +use DateTime; +use DateTimeImmutable; +use DateTimeZone; use Nette\Utils\Json; use Phar; -use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; -use PHPStan\Analyser\ResultCache\ResultCacheClearer; -use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\InternalError; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileMonitor; use PHPStan\File\FileMonitorResult; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; -use PHPStan\Parallel\Scheduler; -use PHPStan\Process\CpuCoreCounter; +use PHPStan\Internal\ComposerHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\PhpDoc\StubFilesProvider; +use PHPStan\Process\ProcessCanceledException; +use PHPStan\Process\ProcessCrashedException; use PHPStan\Process\ProcessHelper; use PHPStan\Process\ProcessPromise; -use PHPStan\Process\Runnable\RunnableQueue; -use PHPStan\Process\Runnable\RunnableQueueLogger; +use PHPStan\ShouldNotHappenException; use Psr\Http\Message\ResponseInterface; use React\ChildProcess\Process; +use React\Dns\Config\Config; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Http\Browser; -use React\Promise\CancellablePromiseInterface; -use React\Promise\ExtendedPromiseInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; +use React\Socket\TcpServer; +use React\Stream\ReadableStreamInterface; +use RuntimeException; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use const PHP_BINARY; -use function Clue\React\Block\await; +use Throwable; +use function array_merge; +use function count; +use function defined; use function escapeshellarg; -use function file_exists; -use function React\Promise\resolve; +use function fclose; +use function fopen; +use function fwrite; +use function get_class; +use function getenv; +use function http_build_query; +use function ini_get; +use function is_file; +use function parse_url; +use function React\Async\await; +use function sprintf; +use function strlen; +use function unlink; +use const JSON_INVALID_UTF8_IGNORE; +use const PHP_BINARY; +use const PHP_URL_PORT; +use const PHP_VERSION_ID; -class FixerApplication +#[AutowiredService] +final class FixerApplication { - /** @var FileMonitor */ - private $fileMonitor; - - /** @var ResultCacheManagerFactory */ - private $resultCacheManagerFactory; - - /** @var ResultCacheClearer */ - private $resultCacheClearer; - - /** @var IgnoredErrorHelper */ - private $ignoredErrorHelper; - - /** @var CpuCoreCounter */ - private $cpuCoreCounter; - - /** @var Scheduler */ - private $scheduler; - - /** @var string[] */ - private $analysedPaths; + /** @var PromiseInterface|null */ + private PromiseInterface|null $processInProgress = null; - /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ - private $processInProgress; - - /** @var string */ - private $currentWorkingDirectory; - - /** @var string */ - private $fixerTmpDir; - - private int $maximumNumberOfProcesses; - - /** @var string|null */ - private $fixerSuggestionId; + private bool $fileMonitorActive = true; /** - * @param FileMonitor $fileMonitor - * @param ResultCacheManagerFactory $resultCacheManagerFactory * @param string[] $analysedPaths + * @param list $dnsServers + * @param string[] $composerAutoloaderProjectPaths + * @param string[] $allConfigFiles + * @param string[] $bootstrapFiles */ public function __construct( - FileMonitor $fileMonitor, - ResultCacheManagerFactory $resultCacheManagerFactory, - ResultCacheClearer $resultCacheClearer, - IgnoredErrorHelper $ignoredErrorHelper, - CpuCoreCounter $cpuCoreCounter, - Scheduler $scheduler, - array $analysedPaths, - string $currentWorkingDirectory, - string $fixerTmpDir, - int $maximumNumberOfProcesses + private FileMonitor $fileMonitor, + private IgnoredErrorHelper $ignoredErrorHelper, + private StubFilesProvider $stubFilesProvider, + #[AutowiredParameter] + private array $analysedPaths, + #[AutowiredParameter] + private string $currentWorkingDirectory, + #[AutowiredParameter(ref: '%pro.tmpDir%')] + private string $proTmpDir, + #[AutowiredParameter(ref: '%pro.dnsServers%')] + private array $dnsServers, + #[AutowiredParameter] + private array $composerAutoloaderProjectPaths, + #[AutowiredParameter] + private array $allConfigFiles, + #[AutowiredParameter] + private ?string $cliAutoloadFile, + #[AutowiredParameter] + private array $bootstrapFiles, + #[AutowiredParameter] + private ?string $editorUrl, + #[AutowiredParameter] + private string $usedLevel, ) { - $this->fileMonitor = $fileMonitor; - $this->resultCacheManagerFactory = $resultCacheManagerFactory; - $this->resultCacheClearer = $resultCacheClearer; - $this->ignoredErrorHelper = $ignoredErrorHelper; - $this->cpuCoreCounter = $cpuCoreCounter; - $this->scheduler = $scheduler; - $this->analysedPaths = $analysedPaths; - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->fixerTmpDir = $fixerTmpDir; - $this->maximumNumberOfProcesses = $maximumNumberOfProcesses; } - /** - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @param \PHPStan\Analyser\Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - * @return int - */ public function run( ?string $projectConfigFile, - InceptionResult $inceptionResult, InputInterface $input, OutputInterface $output, - array $fileSpecificErrors, - array $notFileSpecificErrors, int $filesCount, - string $mainScript + string $mainScript, ): int { $loop = new StreamSelectLoop(); - $server = new \React\Socket\TcpServer('127.0.0.1:0', $loop); + $server = new TcpServer('127.0.0.1:0', $loop); /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url($serverAddress, PHP_URL_PORT); - $reanalyseProcessQueue = new RunnableQueue( - new class () implements RunnableQueueLogger { - - public function log(string $message): void - { - } - - }, - min($this->cpuCoreCounter->getNumberOfCpuCores(), $this->maximumNumberOfProcesses) - ); - - $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $fileSpecificErrors, $notFileSpecificErrors, $mainScript, $filesCount, $reanalyseProcessQueue, $inceptionResult): void { - $decoder = new Decoder($connection, true, 512, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, 128 * 1024 * 1024); - $encoder = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); + $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, options: $jsonInvalidUtf8Ignore, maxlength: 128 * 1024 * 1024); + $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $encoder->write(['action' => 'initialData', 'data' => [ - 'fileSpecificErrors' => $fileSpecificErrors, - 'notFileSpecificErrors' => $notFileSpecificErrors, 'currentWorkingDirectory' => $this->currentWorkingDirectory, 'analysedPaths' => $this->analysedPaths, 'projectConfigFile' => $projectConfigFile, 'filesCount' => $filesCount, - 'phpstanVersion' => $this->getPhpstanVersion(), + 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'editorUrl' => $this->editorUrl, + 'ruleLevel' => $this->usedLevel, ]]); $decoder->on('data', function (array $data) use ( - $loop, - $encoder, - $projectConfigFile, - $input, $output, - $mainScript, - $reanalyseProcessQueue, - $inceptionResult ): void { if ($data['action'] === 'webPort') { $output->writeln(sprintf('Open your web browser at: http://127.0.0.1:%d', $data['data']['port'])); $output->writeln('Press [Ctrl-C] to quit.'); return; } - if ($data['action'] === 'restoreResultCache') { - $this->fixerSuggestionId = $data['data']['fixerSuggestionId']; + if ($data['action'] === 'resumeFileMonitor') { + $this->fileMonitorActive = true; + return; } - if ($data['action'] !== 'reanalyse') { + if ($data['action'] === 'pauseFileMonitor') { + $this->fileMonitorActive = false; return; } + }); - $id = $data['id']; + $this->fileMonitor->initialize(array_merge( + $this->getComposerLocks(), + $this->getComposerInstalled(), + $this->getExecutedFiles(), + $this->getStubFiles(), + $this->allConfigFiles, + )); - $this->reanalyseWithTmpFile( - $loop, - $inceptionResult, - $mainScript, - $reanalyseProcessQueue, - $projectConfigFile, - $data['data']['tmpFile'], - $data['data']['insteadOfFile'], - $data['data']['fixerSuggestionId'], - $input - )->done(static function (string $output) use ($encoder, $id): void { - $encoder->write(['id' => $id, 'response' => Json::decode($output, Json::FORCE_ARRAY)]); - }, static function (\Throwable $e) use ($encoder, $id, $output): void { - if ($e instanceof \PHPStan\Process\ProcessCrashedException) { - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - if ($e instanceof \PHPStan\Process\ProcessCanceledException) { - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - - $output->writeln('Unexpected error: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - }); - }); + $this->analyse( + $loop, + $mainScript, + $projectConfigFile, + $input, + $output, + $encoder, + ); - $this->fileMonitor->initialize($this->analysedPaths); - $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output, $reanalyseProcessQueue, $inceptionResult): void { - $reanalyseProcessQueue->cancelAll(); + $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output): void { if ($this->processInProgress !== null) { $this->processInProgress->cancel(); $this->processInProgress = null; - } else { - $encoder->write(['action' => 'analysisStart']); } - $this->reanalyseAfterFileChanges( + if (count($changes->getChangedFiles()) > 0) { + $encoder->write(['action' => 'changedFiles', 'data' => [ + 'paths' => $changes->getChangedFiles(), + ]]); + } + + $this->analyse( $loop, - $inceptionResult, $mainScript, $projectConfigFile, - $this->fixerSuggestionId, - $input - )->done(function (array $json) use ($encoder, $changes): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $encoder->write(['action' => 'analysisEnd', 'data' => [ - 'fileSpecificErrors' => $json['fileSpecificErrors'], - 'notFileSpecificErrors' => $json['notFileSpecificErrors'], - 'filesCount' => $changes->getTotalFilesCount(), - ]]); - $this->resultCacheClearer->clearTemporaryCaches(); - }, function (\Throwable $e) use ($encoder, $output): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['action' => 'analysisCrash', 'data' => [ - 'error' => $e->getMessage(), - ]]); - }); + $input, + $output, + $encoder, + ); }); }); try { $fixerProcess = $this->getFixerProcess($output, $serverPort); - } catch (\PHPStan\Command\FixerProcessException $e) { + } catch (FixerProcessException) { return 1; } $fixerProcess->start($loop); - $fixerProcess->on('exit', static function ($exitCode) use ($output, $loop): void { + $fixerProcess->on('exit', function ($exitCode) use ($output, $loop): void { $loop->stop(); if ($exitCode === null) { return; @@ -261,6 +215,7 @@ public function log(string $message): void return; } $output->writeln(sprintf('PHPStan Pro process exited with code %d.', $exitCode)); + @unlink($this->proTmpDir . '/phar-info.json'); }); $loop->run(); @@ -273,22 +228,23 @@ public function log(string $message): void */ private function getFixerProcess(OutputInterface $output, int $serverPort): Process { - if (!@mkdir($this->fixerTmpDir, 0777) && !is_dir($this->fixerTmpDir)) { - $output->writeln(sprintf('Cannot create a temp directory %s', $this->fixerTmpDir)); - throw new \PHPStan\Command\FixerProcessException(); + try { + DirectoryCreator::ensureDirectoryExists($this->proTmpDir, 0777); + } catch (DirectoryCreatorException $e) { + $output->writeln($e->getMessage()); + throw new FixerProcessException(); } - $pharPath = $this->fixerTmpDir . '/phpstan-fixer.phar'; - $infoPath = $this->fixerTmpDir . '/phar-info.json'; + $pharPath = $this->proTmpDir . '/phpstan-fixer.phar'; + $infoPath = $this->proTmpDir . '/phar-info.json'; try { $this->downloadPhar($output, $pharPath, $infoPath); - } catch (\RuntimeException $e) { - if (!file_exists($pharPath)) { - $output->writeln('Could not download the PHPStan Pro executable.'); - $output->writeln($e->getMessage()); + } catch (RuntimeException $e) { + if (!is_file($pharPath)) { + $this->printDownloadError($output, $e); - throw new \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } } @@ -297,23 +253,26 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc try { $phar = new Phar($pharPath); - } catch (\Throwable $e) { + } 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 \PHPStan\Command\FixerProcessException(); + 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 \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } - $env = null; + $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']; @@ -325,7 +284,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(sprintf(' -p 127.0.0.1:%d:%d', $_SERVER['PHPSTAN_PRO_WEB_PORT'], $_SERVER['PHPSTAN_PRO_WEB_PORT'])); $output->writeln('2) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } else { @@ -341,55 +300,69 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(' -p 127.0.0.1:11111:11111'); $output->writeln('4) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } - return new Process(sprintf('%s -d memory_limit=%s %s --port %d', PHP_BINARY, escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); + return new Process(sprintf('%s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), env: $env, fds: []); } private function downloadPhar( OutputInterface $output, string $pharPath, - string $infoPath + string $infoPath, ): void { $currentVersion = null; - if (file_exists($pharPath) && file_exists($infoPath)) { - /** @var array{version: string, date: string} $currentInfo */ + $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']; - $currentDate = \DateTime::createFromFormat(\DateTime::ATOM, $currentInfo['date']); + $currentBranch = $currentInfo['branch'] ?? 'master'; + $currentDate = DateTime::createFromFormat(DateTime::ATOM, $currentInfo['date']); if ($currentDate === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ((new \DateTimeImmutable('', new \DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours')) { + if ( + $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; + $loop = new StreamSelectLoop(); + + // @phpstan-ignore staticMethod.internal (required because of the await() call below) + Loop::set($loop); + $client = new Browser( - $loop, new Connector( - $loop, [ 'timeout' => 5, 'tls' => [ 'cafile' => CaBundle::getBundledCaBundlePath(), ], - 'dns' => '1.1.1.1', - ] - ) + 'dns' => $dnsConfig, + ], + $loop, + ), + $loop, ); - /** @var array{url: string, version: string} $latestInfo */ - $latestInfo = Json::decode((string) await($client->get('https://fixer-download-api.phpstan.com/latest'), $loop, 5.0)->getBody(), Json::FORCE_ARRAY); // @phpstan-ignore-line + /** + * @var array{url: string, version: string} $latestInfo + */ + $latestInfo = Json::decode((string) await($client->get(sprintf('https://fixer-download-api.phpstan.com/latest?%s', http_build_query(['phpVersion' => PHP_VERSION_ID, 'branch' => $branch]))))->getBody(), Json::FORCE_ARRAY); if ($currentVersion !== null && $latestInfo['version'] === $currentVersion) { - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); $output->writeln('You\'re running the latest PHPStan Pro!'); return; } @@ -398,13 +371,13 @@ private function downloadPhar( $pharPathResource = fopen($pharPath, 'w'); if ($pharPathResource === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); + throw new ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); } $progressBar = new ProgressBar($output); - $client->requestStreaming('GET', $latestInfo['url'])->done(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { + $client->requestStreaming('GET', $latestInfo['url'])->then(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { $body = $response->getBody(); - if (!$body instanceof \React\Stream\ReadableStreamInterface) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$body instanceof ReadableStreamInterface) { + throw new ShouldNotHappenException(); } $totalSize = (int) $response->getHeaderLine('Content-Length'); @@ -418,8 +391,8 @@ private function downloadPhar( fwrite($pharPathResource, $chunk); $progressBar->setProgress($bytes); }); - }, static function (\Throwable $e) use ($output): void { - $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + }, function (Throwable $e) use ($output): void { + $this->printDownloadError($output, $e); }); $loop->run(); @@ -430,24 +403,45 @@ private function downloadPhar( $output->writeln(''); $output->writeln(''); - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); } - private function writeInfoFile(string $infoPath, string $version): void + private function printDownloadError(OutputInterface $output, Throwable $e): void + { + $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + $output->writeln(''); + $output->writeln('Try different DNS servers in your configuration file:'); + $output->writeln(''); + $output->writeln('parameters:'); + $output->writeln("\tpro:"); + $output->writeln("\t\tdnsServers!:"); + $output->writeln("\t\t\t- '8.8.8.8'"); + $output->writeln(''); + } + + private function writeInfoFile(string $infoPath, string $version, string $branch): void { FileWriter::write($infoPath, Json::encode([ 'version' => $version, - 'date' => (new \DateTimeImmutable('', new \DateTimeZone('UTC')))->format(\DateTime::ATOM), + 'branch' => $branch, + 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), ])); } /** - * @param LoopInterface $loop * @param callable(FileMonitorResult): void $hasChangesCallback */ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void { $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()) { @@ -459,133 +453,155 @@ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCal $loop->addTimer(1.0, $callback); } - private function reanalyseWithTmpFile( + private function analyse( LoopInterface $loop, - InceptionResult $inceptionResult, string $mainScript, - RunnableQueue $runnableQueue, ?string $projectConfigFile, - string $tmpFile, - string $insteadOfFile, - string $fixerSuggestionId, - InputInterface $input - ): PromiseInterface + InputInterface $input, + OutputInterface $output, + Encoder $phpstanFixerEncoder, + ): void { - $resultCacheManager = $this->resultCacheManagerFactory->create([$insteadOfFile => $tmpFile]); - [$inceptionFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $inceptionResult->getProjectConfigArray(), $inceptionResult->getErrorOutput()); - $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $resultCache->getFilesToAnalyse()); + $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + if (count($ignoredErrorHelperResult->getErrors()) > 0) { + throw new ShouldNotHappenException(); + } + + // TCP server for fixer:worker (TCP client) + $server = new TcpServer('127.0.0.1:0', $loop); + /** @var string $serverAddress */ + $serverAddress = $server->getAddress(); + /** @var int<0, 65535> $serverPort */ + $serverPort = parse_url($serverAddress, PHP_URL_PORT); + + $server->on('connection', static function (ConnectionInterface $connection) use ($phpstanFixerEncoder): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: 128 * 1024 * 1024); + $decoder->on('data', static function (array $data) use ($phpstanFixerEncoder): void { + $phpstanFixerEncoder->write($data); + }); + }); - $process = new ProcessPromise($loop, $fixerSuggestionId, ProcessHelper::getWorkerCommand( + $process = new ProcessPromise($loop, ProcessHelper::getWorkerCommand( $mainScript, 'fixer:worker', $projectConfigFile, [ - '--tmp-file', - escapeshellarg($tmpFile), - '--instead-of', - escapeshellarg($insteadOfFile), - '--save-result-cache', - escapeshellarg($fixerSuggestionId), - '--allow-parallel', + '--server-port', + (string) $serverPort, ], - $input + $input, )); + $this->processInProgress = $process->run(); + + $this->processInProgress->then(function () use ($server): void { + $this->processInProgress = null; + $server->close(); + }, function (Throwable $e) use ($server, $phpstanFixerEncoder): void { + $this->processInProgress = null; + $server->close(); - return $runnableQueue->queue($process, $schedule->getNumberOfProcesses()); + if ($e instanceof ProcessCanceledException) { + return; + } + + if ($e instanceof ProcessCrashedException) { + $message = 'Analysis crashed'; + $traceAsString = $e->getMessage(); + $trace = []; + } else { + $message = $e->getMessage(); + $traceAsString = $e->getTraceAsString(); + $trace = InternalError::prepareTrace($e); + } + $phpstanFixerEncoder->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => [new InternalError( + $message, + 'running PHPStan Pro worker', + $trace, + $traceAsString, + false, + )], + ]]); + }); } - private function reanalyseAfterFileChanges( - LoopInterface $loop, - InceptionResult $inceptionResult, - string $mainScript, - ?string $projectConfigFile, - ?string $fixerSuggestionId, - InputInterface $input - ): PromiseInterface + private function isDockerRunning(): bool { - $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); - if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + return is_file('/.dockerenv'); + } + + /** + * @return list + */ + private function getComposerLocks(): array + { + $locks = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $lockPath = $autoloadPath . '/composer.lock'; + if (!is_file($lockPath)) { + continue; + } + + $locks[] = $lockPath; } - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - - $resultCacheManager = $this->resultCacheManagerFactory->create([]); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $fixerSuggestionId); - if (count($resultCache->getFilesToAnalyse()) === 0) { - $result = $resultCacheManager->process( - new AnalyserResult([], [], [], [], false), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - true - )->getAnalyserResult(); - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit() - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; - } + return $locks; + } - $finalFileSpecificErrors[] = $intermediateError; + /** + * @return list + */ + private function getComposerInstalled(): array + { + $files = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + if ($composer === null) { + continue; } - return resolve([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]); - } + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($filePath)) { + continue; + } - $options = ['--save-result-cache', '--allow-parallel']; - if ($fixerSuggestionId !== null) { - $options[] = '--restore-result-cache'; - $options[] = $fixerSuggestionId; + $files[] = $filePath; } - $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( - $mainScript, - 'fixer:worker', - $projectConfigFile, - $options, - $input - )); - $this->processInProgress = $process->run(); - return $this->processInProgress->then(static function (string $output): array { - return Json::decode($output, Json::FORCE_ARRAY); - }); + return $files; } - private function getPhpstanVersion(): string + /** + * @return list + */ + private function getExecutedFiles(): array { - try { - return \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - return 'Version unknown'; + $files = []; + if ($this->cliAutoloadFile !== null) { + $files[] = $this->cliAutoloadFile; } - } - private function isDockerRunning(): bool - { - if (!is_file('/proc/1/cgroup')) { - return false; + foreach ($this->bootstrapFiles as $bootstrapFile) { + $files[] = $bootstrapFile; } - try { - $contents = FileReader::read('/proc/1/cgroup'); + return $files; + } - return strpos($contents, 'docker') !== false; - } catch (\PHPStan\File\CouldNotReadFileException $e) { - return false; + /** + * @return list + */ + private function getStubFiles(): array + { + $stubFiles = []; + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { + $stubFiles[] = $stubFile; } + + return $stubFiles; } } diff --git a/src/Command/FixerProcessException.php b/src/Command/FixerProcessException.php index 996000c6ad..c9e4097d58 100644 --- a/src/Command/FixerProcessException.php +++ b/src/Command/FixerProcessException.php @@ -2,7 +2,9 @@ namespace PHPStan\Command; -class FixerProcessException extends \Exception +use Exception; + +final class FixerProcessException extends Exception { } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index 3a82fac66f..8701f1045d 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -2,56 +2,80 @@ namespace PHPStan\Command; -use Nette\Utils\Json; +use Clue\React\NDJson\Encoder; +use Override; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Error; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoredErrorHelperResult; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; +use PHPStan\Parallel\ParallelAnalyser; +use PHPStan\Parallel\Scheduler; +use PHPStan\Process\CpuCoreCounter; +use PHPStan\ShouldNotHappenException; +use React\EventLoop\LoopInterface; +use React\EventLoop\StreamSelectLoop; +use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\TcpConnector; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; - -class FixerWorkerCommand extends Command +use function array_diff; +use function array_key_exists; +use function count; +use function filemtime; +use function in_array; +use function is_array; +use function is_bool; +use function is_file; +use function is_string; +use function memory_get_peak_usage; +use function React\Promise\resolve; +use function sprintf; +use function usort; +use const JSON_INVALID_UTF8_IGNORE; + +final class FixerWorkerCommand extends Command { private const NAME = 'fixer:worker'; - /** @var string[] */ - private $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } + #[Override] protected function configure(): void { $this->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('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('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - new InputOption('save-result-cache', null, InputOption::VALUE_OPTIONAL, '', false), - new InputOption('restore-result-cache', null, InputOption::VALUE_REQUIRED), - new InputOption('allow-parallel', null, InputOption::VALUE_NONE, 'Allow parallel analysis'), - ]); + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for analysis'), + new InputOption('xdebug', mode: InputOption::VALUE_NONE, description: 'Allow running with Xdebug for debugging purposes'), + new InputOption('server-port', mode: InputOption::VALUE_REQUIRED, description: 'Server port for FixerApplication'), + ]) + ->setHidden(true); } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $paths = $input->getArgument('paths'); @@ -59,9 +83,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); - $allowParallel = $input->getOption('allow-parallel'); + $serverPort = $input->getOption('server-port'); if ( !is_array($paths) @@ -69,37 +92,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) - || (!is_bool($allowParallel)) + || (!is_string($serverPort)) ) { - throw new \PHPStan\ShouldNotHappenException(); - } - - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - /** @var false|string|null $saveResultCache */ - $saveResultCache = $input->getOption('save-result-cache'); - - /** @var string|null $restoreResultCache */ - $restoreResultCache = $input->getOption('restore-result-cache'); - if (is_string($tmpFile)) { - if (!is_string($insteadOfFile)) { - throw new \PHPStan\ShouldNotHappenException(); - } - } elseif (is_string($insteadOfFile)) { - throw new \PHPStan\ShouldNotHappenException(); - } elseif ($saveResultCache === false) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; + throw new ShouldNotHappenException(); } try { @@ -107,7 +103,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -116,11 +111,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, + null, + null, false, - $singleReflectionFile, - $insteadOfFile ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { return 1; } @@ -130,132 +125,296 @@ protected function execute(InputInterface $input, OutputInterface $output): int $ignoredErrorHelper = $container->getByType(IgnoredErrorHelper::class); $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - /** @var AnalyserRunner $analyserRunner */ - $analyserRunner = $container->getByType(AnalyserRunner::class); - - $fileReplacements = []; - if ($insteadOfFile !== null && $tmpFile !== null) { - $fileReplacements = [$insteadOfFile => $tmpFile]; - } - /** @var ResultCacheManager $resultCacheManager */ - $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create($fileReplacements); - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $restoreResultCache); - - $intermediateAnalyserResult = $analyserRunner->runAnalyser( - $resultCache->getFilesToAnalyse(), - $inceptionFiles, - null, - null, - false, - $allowParallel, - $configuration, - $tmpFile, - $insteadOfFile, - $input - ); - $result = $resultCacheManager->process( - $this->switchTmpFileInAnalyserResult($intermediateAnalyserResult, $tmpFile, $insteadOfFile), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null - )->getAnalyserResult(); - - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit() - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; + $loop = new StreamSelectLoop(); + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->then(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $out = new Encoder($connection, $jsonInvalidUtf8Ignore); + //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + + /** @var ResultCacheManager $resultCacheManager */ + $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create([]); + $projectConfigArray = $inceptionResult->getProjectConfigArray(); + + /** @var AnalyserResultFinalizer $analyserResultFinalizer */ + $analyserResultFinalizer = $container->getByType(AnalyserResultFinalizer::class); + + try { + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); + } catch (PathNotFoundException | InceptionNotSuccessfulException) { + throw new ShouldNotHappenException(); } - $finalFileSpecificErrors[] = $intermediateError; - } + $out->write([ + 'action' => 'analysisStart', + 'result' => [ + 'analysedFiles' => $inceptionFiles, + ], + ]); - $output->writeln(Json::encode([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]), OutputInterface::OUTPUT_RAW); + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); - return 0; - } + $errorsFromResultCacheTmp = $resultCache->getErrors(); + $locallyIgnoredErrorsFromResultCacheTmp = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $fileToAnalyse) { + unset($errorsFromResultCacheTmp[$fileToAnalyse]); + unset($locallyIgnoredErrorsFromResultCacheTmp[$fileToAnalyse]); + } - private function switchTmpFileInAnalyserResult( - AnalyserResult $analyserResult, - ?string $insteadOfFile, - ?string $tmpFile - ): AnalyserResult - { - $fileSpecificErrors = []; - foreach ($analyserResult->getErrors() as $error) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - ) { - if ($error->getFilePath() === $insteadOfFile) { - $error = $error->changeFilePath($tmpFile); + $errorsFromResultCache = []; + foreach ($errorsFromResultCacheTmp as $errorsByFile) { + foreach ($errorsByFile as $error) { + $errorsFromResultCache[] = $error; } - if ($error->getTraitFilePath() === $insteadOfFile) { - $error = $error->changeTraitFilePath($tmpFile); + } + + [$errorsFromResultCache, $ignoredErrorsFromResultCache] = $this->filterErrors($errorsFromResultCache, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); + + foreach ($locallyIgnoredErrorsFromResultCacheTmp as $locallyIgnoredErrors) { + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrorsFromResultCache[] = [$locallyIgnoredError, null]; } } - $fileSpecificErrors[] = $error; - } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $errorsFromResultCache, + 'ignoredErrors' => $ignoredErrorsFromResultCache, + 'analysedFiles' => array_diff($inceptionFiles, $resultCache->getFilesToAnalyse()), + ], + ]); + + $filesToAnalyse = $resultCache->getFilesToAnalyse(); + usort($filesToAnalyse, static function (string $a, string $b): int { + $aTime = @filemtime($a); + if ($aTime === false) { + return 1; + } + + $bTime = @filemtime($b); + if ($bTime === false) { + return -1; + } + + // files are sorted from the oldest + // because ParallelAnalyser reverses the scheduler jobs to do the smallest + // jobs first + return $aTime <=> $bTime; + }); + + $this->runAnalyser( + $loop, + $container, + $filesToAnalyse, + $configuration, + $input, + function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ($out, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles): void { + $internalErrors = []; + foreach ($errors as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } + + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); + } + + 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(), + ); + } - $dependencies = null; - if ($analyserResult->getDependencies() !== null) { - $dependencies = []; - foreach ($analyserResult->getDependencies() as $dependencyFile => $dependentFiles) { - $new = []; - foreach ($dependentFiles as $file) { - if ($file === $insteadOfFile && $tmpFile !== null) { - $new[] = $tmpFile; + foreach ($finalizerResult->getAnalyserResult()->getUnorderedErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { continue; } - $new[] = $file; + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); } - $key = $dependencyFile; - if ($key === $insteadOfFile && $tmpFile !== null) { - $key = $tmpFile; + $hasInternalErrors = count($internalErrors) > 0 || $finalizerResult->getAnalyserResult()->hasReachedInternalErrorsCountLimit(); + + if ($hasInternalErrors) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => count($internalErrors) > 0 ? $internalErrors : [ + new InternalError( + 'Internal error occurred', + 'running analyser in PHPStan Pro worker', + [], + null, + false, + ), + ], + ]]); } - $dependencies[$key] = $new; - } + [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($finalizerResult->getCollectorErrors(), $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); + foreach ($finalizerResult->getLocallyIgnoredCollectorErrors() as $locallyIgnoredCollectorError) { + $ignoredCollectorErrors[] = [$locallyIgnoredCollectorError, null]; + } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $collectorErrors, + 'ignoredErrors' => $ignoredCollectorErrors, + 'analysedFiles' => [], + ], + ]); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process( + $finalizerResult->getErrors(), + $isOnlyFiles, + $inceptionFiles, + $hasInternalErrors, + ); + $ignoreFileErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + continue; + } + if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier'], true)) { + continue; + } + $ignoreFileErrors[] = $error; + } + + $out->end([ + 'action' => 'analysisEnd', + 'result' => [ + 'ignoreFileErrors' => $ignoreFileErrors, + 'ignoreNotFileErrors' => $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(), + ], + ]); + }); + }); + $loop->run(); + + return 0; + } + + private function transformErrorIntoInternalError(Error $error): InternalError + { + $message = $error->getMessage(); + $metadata = $error->getMetadata(); + if ( + $error->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); } - $exportedNodes = []; - foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - && $file === $insteadOfFile - ) { - $file = $tmpFile; + return new InternalError( + $message, + sprintf('analysing file %s', $error->getTraitFilePath() ?? $error->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ); + } + + /** + * @param string[] $inceptionFiles + * @param array $errors + * @return array{list, list} + */ + private function filterErrors(array $errors, IgnoredErrorHelperResult $ignoredErrorHelperResult, bool $onlyFiles, array $inceptionFiles, bool $hasInternalErrors): array + { + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $inceptionFiles, $hasInternalErrors); + $finalErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + $finalErrors[] = $error; + continue; } + if (in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched'], true)) { + continue; + } + $finalErrors[] = $error; + } - $exportedNodes[$file] = $fileExportedNodes; + 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 new AnalyserResult( - $fileSpecificErrors, - $analyserResult->getInternalErrors(), - $dependencies, - $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit() + return $parallelAnalyser->analyse( + $loop, + $schedule, + $mainScript, + null, + $configuration, + null, + null, + $input, + $onFileAnalysisHandler, ); } diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index 7e648f86e8..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,28 +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, '||', '\|\|'); - } - if ( - strpos($regex, '()') !== false - && strpos($e->getMessage(), 'Unexpected token ")" (_capturing) at line 1') === 0 - ) { - return new IgnoredRegexValidatorResult([], false, true, '()', '\(\)'); - } + } catch (Exception) { return new IgnoredRegexValidatorResult([], false, false); } + if (Strings::match($regex, '~(?getIgnoredTypes($ast), $this->hasAnchorsInTheMiddle($ast), - false + false, ); } /** - * @param TreeNode $ast * @return array */ private function getIgnoredTypes(TreeNode $ast): array @@ -83,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; @@ -106,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 0acda5dd11..0906a3c84b 100644 --- a/src/Command/IgnoredRegexValidatorResult.php +++ b/src/Command/IgnoredRegexValidatorResult.php @@ -2,38 +2,20 @@ namespace PHPStan\Command; -class IgnoredRegexValidatorResult +final class IgnoredRegexValidatorResult { - /** @var array */ - private array $ignoredTypes; - - private bool $anchorsInTheMiddle; - - private bool $allErrorsIgnored; - - private ?string $wrongSequence; - - private ?string $escapedWrongSequence; - /** * @param array $ignoredTypes - * @param bool $anchorsInTheMiddle - * @param bool $allErrorsIgnored */ public function __construct( - array $ignoredTypes, - bool $anchorsInTheMiddle, - bool $allErrorsIgnored, - ?string $wrongSequence = null, - ?string $escapedWrongSequence = null + private array $ignoredTypes, + private bool $anchorsInTheMiddle, + private bool $allErrorsIgnored, + private ?string $wrongSequence = null, + private ?string $escapedWrongSequence = null, ) { - $this->ignoredTypes = $ignoredTypes; - $this->anchorsInTheMiddle = $anchorsInTheMiddle; - $this->allErrorsIgnored = $allErrorsIgnored; - $this->wrongSequence = $wrongSequence; - $this->escapedWrongSequence = $escapedWrongSequence; } /** diff --git a/src/Command/InceptionNotSuccessfulException.php b/src/Command/InceptionNotSuccessfulException.php index 872b71cfaa..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 d0b778a9d8..13ff361922 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -3,73 +3,52 @@ namespace PHPStan\Command; use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; use PHPStan\Internal\BytesHelper; +use function floor; +use function implode; +use function max; use function memory_get_peak_usage; +use function microtime; +use function round; +use function sprintf; -class InceptionResult +final class InceptionResult { /** @var callable(): (array{string[], bool}) */ private $filesCallback; - private Output $stdOutput; - - private Output $errorOutput; - - private \PHPStan\DependencyInjection\Container $container; - - private bool $isDefaultLevelUsed; - - private string $memoryLimitFile; - - private ?string $projectConfigFile; - - /** @var mixed[]|null */ - private ?array $projectConfigArray; - - private ?string $generateBaselineFile; - /** * @param callable(): (array{string[], bool}) $filesCallback - * @param Output $stdOutput - * @param Output $errorOutput - * @param \PHPStan\DependencyInjection\Container $container - * @param bool $isDefaultLevelUsed - * @param string $memoryLimitFile - * @param string|null $projectConfigFile - * @param mixed[] $projectConfigArray - * @param string|null $generateBaselineFile + * @param mixed[]|null $projectConfigArray */ public function __construct( callable $filesCallback, - Output $stdOutput, - Output $errorOutput, - Container $container, - bool $isDefaultLevelUsed, - string $memoryLimitFile, - ?string $projectConfigFile, - ?array $projectConfigArray, - ?string $generateBaselineFile + private Output $stdOutput, + private Output $errorOutput, + private Container $container, + private bool $isDefaultLevelUsed, + private ?string $projectConfigFile, + private ?array $projectConfigArray, + private ?string $generateBaselineFile, + private ?string $editorModeTmpFile, + private ?string $editorModeInsteadOfFile, ) { $this->filesCallback = $filesCallback; - $this->stdOutput = $stdOutput; - $this->errorOutput = $errorOutput; - $this->container = $container; - $this->isDefaultLevelUsed = $isDefaultLevelUsed; - $this->memoryLimitFile = $memoryLimitFile; - $this->projectConfigFile = $projectConfigFile; - $this->projectConfigArray = $projectConfigArray; - $this->generateBaselineFile = $generateBaselineFile; } /** + * @throws InceptionNotSuccessfulException + * @throws PathNotFoundException * @return array{string[], bool} */ public function getFiles(): array { $callback = $this->filesCallback; + /** @throws InceptionNotSuccessfulException|PathNotFoundException */ return $callback(); } @@ -111,14 +90,56 @@ public function getGenerateBaselineFile(): ?string return $this->generateBaselineFile; } - public function handleReturn(int $exitCode): int + public function getEditorModeTmpFile(): ?string + { + return $this->editorModeTmpFile; + } + + public function getEditorModeInsteadOfFile(): ?string + { + return $this->editorModeInsteadOfFile; + } + + public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes, float $analysisStartTime): int { if ($this->getErrorOutput()->isVerbose()) { - $this->getErrorOutput()->writeLineFormatted(sprintf('Used memory: %s', BytesHelper::bytes(memory_get_peak_usage(true)))); + $elapsedTime = round(microtime(true) - $analysisStartTime, 2); + if ($elapsedTime < 10) { + $elapsedTimeString = sprintf('%.2f seconds', $elapsedTime); + } else { + $elapsedTimeString = $this->formatDuration((int) $elapsedTime); + } + $this->getErrorOutput()->writeLineFormatted(sprintf( + 'Elapsed time: %s', + $elapsedTimeString, + )); + } + + 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 formatDuration(int $seconds): string + { + $minutes = (int) floor($seconds / 60); + $remainingSeconds = $seconds % 60; + + $result = []; + if ($minutes > 0) { + $result[] = $minutes . ' minute' . ($minutes > 1 ? 's' : ''); + } + + if ($remainingSeconds > 0) { + $result[] = $remainingSeconds . ' second' . ($remainingSeconds > 1 ? 's' : ''); + } + + return implode(' ', $result); + } + } diff --git a/src/Command/Output.php b/src/Command/Output.php index 34b26c6c18..b0efcd648a 100644 --- a/src/Command/Output.php +++ b/src/Command/Output.php @@ -16,6 +16,10 @@ public function getStyle(): OutputStyle; public function isVerbose(): bool; + public function isVeryVerbose(): bool; + public function isDebug(): bool; + public function isDecorated(): bool; + } diff --git a/src/Command/Symfony/SymfonyOutput.php b/src/Command/Symfony/SymfonyOutput.php index ba12d8206d..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,9 +44,19 @@ public function isVerbose(): bool return $this->symfonyOutput->isVerbose(); } + public function isVeryVerbose(): bool + { + return $this->symfonyOutput->isVeryVerbose(); + } + public function isDebug(): bool { return $this->symfonyOutput->isDebug(); } + public function isDecorated(): bool + { + return $this->symfonyOutput->isDecorated(); + } + } diff --git a/src/Command/Symfony/SymfonyStyle.php b/src/Command/Symfony/SymfonyStyle.php index ba2f25ee16..4008ec0b2a 100644 --- a/src/Command/Symfony/SymfonyStyle.php +++ b/src/Command/Symfony/SymfonyStyle.php @@ -8,19 +8,11 @@ /** * @internal */ -class SymfonyStyle implements OutputStyle +final class SymfonyStyle implements OutputStyle { - private \Symfony\Component\Console\Style\StyleInterface $symfonyStyle; - - public function __construct(StyleInterface $symfonyStyle) - { - $this->symfonyStyle = $symfonyStyle; - } - - public function getSymfonyStyle(): \Symfony\Component\Console\Style\StyleInterface + public function __construct(private StyleInterface $symfonyStyle) { - return $this->symfonyStyle; } public function title(string $message): void diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index e1154d3829..dfef585109 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -4,10 +4,15 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; +use Override; 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; @@ -18,47 +23,57 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; +use function array_fill_keys; +use function array_filter; +use function array_merge; +use function array_unshift; +use function array_values; +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; } + #[Override] protected function configure(): void { $this->setName(self::NAME) ->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('port', null, InputOption::VALUE_REQUIRED), - new InputOption('identifier', null, InputOption::VALUE_REQUIRED), - new InputOption('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - ]); + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED, description: 'Memory limit for analysis'), + new InputOption('xdebug', mode: InputOption::VALUE_NONE, description: 'Allow running with Xdebug for debugging purposes'), + new InputOption('port', mode: InputOption::VALUE_REQUIRED), + new InputOption('identifier', mode: InputOption::VALUE_REQUIRED), + new InputOption('tmp-file', mode: InputOption::VALUE_REQUIRED), + new InputOption('instead-of', mode: InputOption::VALUE_REQUIRED), + ]) + ->setHidden(true); } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $paths = $input->getArgument('paths'); @@ -66,10 +81,11 @@ 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'); + $tmpFile = $input->getOption('tmp-file'); + $insteadOfFile = $input->getOption('instead-of'); if ( !is_array($paths) @@ -77,23 +93,13 @@ 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) + || (!is_string($tmpFile) && $tmpFile !== null) + || (!is_string($insteadOfFile) && $insteadOfFile !== null) ) { - throw new \PHPStan\ShouldNotHappenException(); - } - - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; + throw new ShouldNotHappenException(); } try { @@ -101,7 +107,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -110,10 +115,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, + $tmpFile, + $insteadOfFile, false, - $singleReflectionFile ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } $loop = new StreamSelectLoop(); @@ -123,21 +129,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$analysedFiles] = $inceptionResult->getFiles(); $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); - } catch (\PHPStan\File\PathNotFoundException $e) { + } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + return 1; } - /** @var NodeScopeResolver $nodeScopeResolver */ $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($analysedFiles); $analysedFiles = array_fill_keys($analysedFiles, true); - $tcpConector = new TcpConnector($loop); - $tcpConector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles, $tmpFile, $insteadOfFile): void { - $out = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); - $in = new Decoder($connection, true, 512, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, $container->getParameter('parallel')['buffer']); + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $port))->then(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles, $tmpFile, $insteadOfFile): 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, options: $jsonInvalidUtf8Ignore, maxlength: $container->getParameter('parallel')['buffer']); $out->write(['action' => 'hello', 'identifier' => $identifier]); $this->runWorker($container, $out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile); }); @@ -152,13 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param Container $container - * @param WritableStreamInterface $out - * @param ReadableStreamInterface $in - * @param OutputInterface $output * @param array $analysedFiles - * @param string|null $tmpFile - * @param string|null $insteadOfFile */ private function runWorker( Container $container, @@ -167,32 +171,45 @@ private function runWorker( OutputInterface $output, array $analysedFiles, ?string $tmpFile, - ?string $insteadOfFile + ?string $insteadOfFile, ): void { - $handleError = function (\Throwable $error) use ($out, $output): void { + $handleError = function (Throwable $error) use ($out, $output): void { $this->errorCount++; $output->writeln(sprintf('Error: %s', $error->getMessage())); $out->write([ 'action' => 'result', 'result' => [ - 'errors' => [$error->getMessage()], + 'errors' => [], + 'internalErrors' => [ + new InternalError( + $error->getMessage(), + 'communicating with main process in parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + true, + ), + ], + 'filteredPhpErrors' => [], + 'allPhpErrors' => [], + 'locallyIgnoredErrors' => [], + 'linesToIgnore' => [], + 'unmatchedLineIgnores' => [], + 'collectedData' => [], + 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => [], - 'filesCount' => 0, + 'exportedNodes' => [], + 'files' => [], 'internalErrorsCount' => 1, ], ]); $out->end(); }; $out->on('error', $handleError); - - /** @var FileAnalyser $fileAnalyser */ $fileAnalyser = $container->getByType(FileAnalyser::class); - - /** @var Registry $registry */ - $registry = $container->getByType(Registry::class); - - $in->on('data', function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles, $tmpFile, $insteadOfFile): void { + $ruleRegistry = $container->getByType(RuleRegistry::class); + $collectorRegistry = $container->getByType(CollectorRegistry::class); + $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $tmpFile, $insteadOfFile): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -201,31 +218,52 @@ private function runWorker( $internalErrorsCount = 0; $files = $json['files']; $errors = []; + $internalErrors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; + $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; + $collectedData = []; $dependencies = []; + $usedTraitDependencies = []; $exportedNodes = []; foreach ($files as $file) { try { if ($file === $insteadOfFile) { $file = $tmpFile; } - $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $registry, null); + $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); + $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); foreach ($fileErrors as $fileError) { $errors[] = $fileError; } - } catch (\Throwable $t) { - $this->errorCount++; + 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 in file %s', $t->getMessage(), $file); - $internalErrorMessage .= sprintf( - '%sRun PHPStan with --debug option and post the stack trace to:%s%s', - "\n", - "\n", - 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.md' + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('analysing file %s', $file), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, ); - $errors[] = $internalErrorMessage; } } @@ -233,9 +271,18 @@ 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, + 'usedTraitDependencies' => $usedTraitDependencies, 'exportedNodes' => $exportedNodes, - 'filesCount' => count($files), + 'files' => $files, 'internalErrorsCount' => $internalErrorsCount, ]]); }); @@ -244,24 +291,21 @@ private function runWorker( /** * @param string[] $analysedFiles - * @param string|null $insteadOfFile - * @param string|null $tmpFile * @return string[] */ private function switchTmpFile( array $analysedFiles, ?string $insteadOfFile, - ?string $tmpFile + ?string $tmpFile, ): array { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); + if ($insteadOfFile === null) { + return $analysedFiles; + } + $analysedFiles = array_values(array_filter($analysedFiles, static fn (string $file): bool => $file !== $insteadOfFile)); + if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; + array_unshift($analysedFiles, $tmpFile); } return $analysedFiles; diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 5507705a72..95a9484fae 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -2,97 +2,168 @@ namespace PHPStan\Dependency; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Foreach_; use PHPStan\Analyser\Scope; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileHelper; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\InClassMethodNode; +use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Node\InPropertyHookNode; +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\Constant\ConstantStringType; +use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_merge; +use function count; -class DependencyResolver +#[AutowiredService] +final class DependencyResolver { - private FileHelper $fileHelper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private ExportedNodeResolver $exportedNodeResolver; - public function __construct( - FileHelper $fileHelper, - ReflectionProvider $reflectionProvider, - ExportedNodeResolver $exportedNodeResolver + private FileHelper $fileHelper, + private ReflectionProvider $reflectionProvider, + private ExportedNodeResolver $exportedNodeResolver, + private FileTypeMapper $fileTypeMapper, ) { - $this->fileHelper = $fileHelper; - $this->reflectionProvider = $reflectionProvider; - $this->exportedNodeResolver = $exportedNodeResolver; } - public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDependencies + public function resolveDependencies(Node $node, Scope $scope): NodeDependencies { $dependenciesReflections = []; - if ($node instanceof \PhpParser\Node\Stmt\Class_) { + if ($node instanceof Node\Stmt\Class_) { + if (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_) { + } elseif ($node instanceof Node\Stmt\Interface_) { + if ($node->namespacedName !== null) { + $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); + } foreach ($node->extends as $className) { $this->addClassToDependencies($className->toString(), $dependenciesReflections); } + } elseif ($node instanceof Node\Stmt\Enum_) { + if ($node->namespacedName !== null) { + $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); + } + foreach ($node->implements as $className) { + $this->addClassToDependencies($className->toString(), $dependenciesReflections); + } } elseif ($node instanceof InClassMethodNode) { - $nativeMethod = $scope->getFunction(); - if ($nativeMethod !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($nativeMethod->getVariants()); - $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); - if ($parametersAcceptor instanceof \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); + } + } + if ($nativeMethod->getSelfOutType() !== null) { + foreach ($nativeMethod->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } elseif ($node instanceof InPropertyHookNode) { + $nativeMethod = $node->getHookReflection(); + $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); + $this->extractFromParametersAcceptor($nativeMethod, $dependenciesReflections); + } elseif ($node instanceof ClassPropertyNode) { + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { + foreach ($nativeType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + $phpDocType = $node->getPhpDocType(); + if ($phpDocType !== null) { + foreach ($phpDocType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } } elseif ($node instanceof InFunctionNode) { - $functionReflection = $scope->getFunction(); - if ($functionReflection !== null) { - $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $functionReflection = $node->getFunctionReflection(); + $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $this->extractFromParametersAcceptor($functionReflection, $dependenciesReflections); + foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } - } elseif ($node instanceof Closure) { - /** @var ClosureType $closureType */ - $closureType = $scope->getType($node); - foreach ($closureType->getParameters() as $parameter) { - $referencedClasses = $parameter->getType()->getReferencedClasses(); - foreach ($referencedClasses as $referencedClass) { + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + } elseif ($node instanceof Closure || $node instanceof Node\Expr\ArrowFunction) { + $closureType = $scope->getType($node); + if ($closureType instanceof ClosureType) { + foreach ($closureType->getParameters() as $parameter) { + $referencedClasses = $parameter->getType()->getReferencedClasses(); + foreach ($referencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } - $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); - foreach ($returnTypeReferencedClasses as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); + foreach ($returnTypeReferencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - } elseif ($node instanceof \PhpParser\Node\Expr\FuncCall) { + } elseif ($node instanceof Node\Expr\FuncCall) { $functionName = $node->name; - if ($functionName instanceof \PhpParser\Node\Name) { + if ($functionName instanceof Node\Name) { try { - $dependenciesReflections[] = $this->getFunctionReflection($functionName, $scope); - } catch (\PHPStan\Broker\FunctionNotFoundException $e) { + $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 { @@ -104,6 +175,23 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe 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); + } + } } } } @@ -112,8 +200,9 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Expr\MethodCall || $node instanceof \PhpParser\Node\Expr\PropertyFetch) { - $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); } @@ -122,12 +211,63 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe 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->getInstancePropertyReflection($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) { @@ -139,20 +279,162 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + if ($node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $methodClassReflection = $this->reflectionProvider->getClass($className); + if ($methodClassReflection->hasMethod($node->name->toString())) { + $methodReflection = $methodClassReflection->getMethod($node->name->toString(), $scope); + $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + } + } + } else { + $methodReflection = $scope->getMethodReflection($scope->getType($node->class), $node->name->toString()); + if ($methodReflection !== null) { + $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + } + } + } + } elseif ($node instanceof Node\Expr\ClassConstFetch) { + if ($node->class instanceof Node\Name) { + $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } else { + foreach ($scope->getType($node->class)->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + $returnType = $scope->getType($node); + foreach ($returnType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + + if ($node->name instanceof Node\Identifier && $node->name->toLowerString() !== 'class') { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $constantClassReflection = $this->reflectionProvider->getClass($className); + if ($constantClassReflection->hasConstant($node->name->toString())) { + $constantReflection = $constantClassReflection->getConstant($node->name->toString()); + $this->addClassToDependencies($constantReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } else { + $constantReflection = $scope->getConstantReflection($scope->getType($node->class), $node->name->toString()); + if ($constantReflection !== null) { + $this->addClassToDependencies($constantReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } + } elseif ($node instanceof Node\Expr\StaticPropertyFetch) { + if ($node->class instanceof Node\Name) { + $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } else { + foreach ($scope->getType($node->class)->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + $returnType = $scope->getType($node); + foreach ($returnType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + + if ($node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $propertyClassReflection = $this->reflectionProvider->getClass($className); + if ($propertyClassReflection->hasStaticProperty($node->name->toString())) { + $propertyReflection = $propertyClassReflection->getStaticProperty($node->name->toString()); + $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } else { + $propertyReflection = $scope->getStaticPropertyReflection($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); } @@ -166,12 +448,13 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe } 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 ( @@ -192,29 +475,35 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe return new NodeDependencies($this->fileHelper, $dependenciesReflections, $this->exportedNodeResolver->resolve($scope->getFile(), $node)); } - private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): bool + public function resolveUsedTraitDependencies(InClassNode $inClassNode): NodeDependencies { - if (!isset($arrayNode->items[0])) { - return false; + $dependenciesReflections = []; + foreach ($inClassNode->getClassReflection()->getTraits(true) as $trait) { + $dependenciesReflections[] = $trait; } - $itemType = $scope->getType($arrayNode->items[0]->value); - if (!$itemType instanceof ConstantStringType) { - return true; + return new NodeDependencies($this->fileHelper, $dependenciesReflections, null); + } + + private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): bool + { + $items = $arrayNode->items; + if (count($items) !== 2) { + return false; } - return $itemType->isClassString(); + $itemType = $scope->getType($items[0]->value); + return $itemType->isClassString()->yes(); } /** - * @param string $className - * @param array $dependenciesReflections + * @param array $dependenciesReflections */ private function addClassToDependencies(string $className, array &$dependenciesReflections): void { try { $classReflection = $this->reflectionProvider->getClass($className); - } catch (\PHPStan\Broker\ClassNotFoundException $e) { + } catch (ClassNotFoundException) { return; } @@ -225,47 +514,173 @@ private function addClassToDependencies(string $className, array &$dependenciesR $dependenciesReflections[] = $interface; } - foreach ($classReflection->getTraits() as $trait) { + foreach ($classReflection->getTraits(true) as $trait) { $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->getSealedTags() as $sealedTag) { + foreach ($sealedTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getTemplateTags() as $templateTag) { + foreach ($templateTag->getBound()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + + $default = $templateTag->getDefault(); + if ($default === null) { + continue; + } + foreach ($default->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getPropertyTags() as $propertyTag) { + if ($propertyTag->isReadable()) { + foreach ($propertyTag->getReadableType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + if (!$propertyTag->isWritable()) { + continue; + } + + foreach ($propertyTag->getWritableType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getMethodTags() as $methodTag) { + foreach ($methodTag->getReturnType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + foreach ($methodTag->getParameters() as $parameter) { + foreach ($parameter->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + if ($parameter->getDefaultValue() === null) { + continue; + } + foreach ($parameter->getDefaultValue()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + } + + foreach ($classReflection->getExtendsTags() as $extendsTag) { + foreach ($extendsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getImplementsTags() as $implementsTag) { + foreach ($implementsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + $phpDoc = $classReflection->getResolvedPhpDoc(); + if ($phpDoc !== null) { + foreach ($phpDoc->getTypeAliasImportTags() as $importTag) { + $dependenciesReflections[] = $this->reflectionProvider->getClass($importTag->getImportedFrom()); + } + } + $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); } - 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); @@ -273,12 +688,11 @@ private function extractFromParametersAcceptor( } /** - * @param Type|null $throwType - * @param ReflectionWithFilename[] $dependenciesReflections + * @param array $dependenciesReflections */ private function extractThrowType( ?Type $throwType, - array &$dependenciesReflections + array &$dependenciesReflections, ): void { if ($throwType === null) { diff --git a/src/Dependency/ExportedNode.php b/src/Dependency/ExportedNode.php index ee088cd05b..59c32f2a0c 100644 --- a/src/Dependency/ExportedNode.php +++ b/src/Dependency/ExportedNode.php @@ -9,13 +9,11 @@ public function equals(self $node): bool; /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self; /** * @param mixed[] $data - * @return self */ public static function decode(array $data): self; diff --git a/src/Dependency/ExportedNode/ExportedAttributeNode.php b/src/Dependency/ExportedNode/ExportedAttributeNode.php new file mode 100644 index 0000000000..a3079eadce --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -0,0 +1,85 @@ + $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] + #[Override] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'args' => $this->args, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['args'], + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index e9c0523abf..abe08ceea8 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -3,31 +3,25 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedClassConstantNode implements ExportedNode, JsonSerializable +final class ExportedClassConstantNode implements ExportedNode, JsonSerializable { - private string $name; - - private string $value; - - private bool $public; - - private bool $private; - - private bool $final; - - private ?ExportedPhpDocNode $phpDoc; - - public function __construct(string $name, string $value, bool $public, bool $private, bool $final, ?ExportedPhpDocNode $phpDoc) + /** + * @param ExportedAttributeNode[] $attributes + */ + public function __construct( + private string $name, + private string $value, + private array $attributes, + ) { - $this->name = $name; - $this->value = $value; - $this->public = $public; - $this->private = $private; - $this->final = $final; - $this->phpDoc = $phpDoc; } public function equals(ExportedNode $node): bool @@ -36,60 +30,54 @@ public function equals(ExportedNode $node): bool return false; } - if ($this->phpDoc === null) { - if ($node->phpDoc !== null) { - return false; - } - } elseif ($node->phpDoc !== null) { - if (!$this->phpDoc->equals($node->phpDoc)) { + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { return false; } - } else { - return false; } return $this->name === $node->name - && $this->value === $node->value - && $this->public === $node->public - && $this->private === $node->private - && $this->final === $node->final; + && $this->value === $node->value; } /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], $properties['value'], - $properties['public'], - $properties['private'], - $properties['final'], - $properties['phpDoc'] + $properties['attributes'], ); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], $data['value'], - $data['public'], - $data['private'], - $data['final'], - $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null + 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] + #[Override] public function jsonSerialize() { return [ @@ -97,10 +85,7 @@ public function jsonSerialize() 'data' => [ 'name' => $this->name, 'value' => $this->value, - 'public' => $this->public, - 'private' => $this->private, - 'final' => $this->final, - 'phpDoc' => $this->phpDoc, + 'attributes' => $this->attributes, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php new file mode 100644 index 0000000000..ed4fe82198 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php @@ -0,0 +1,108 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->constants) !== count($node->constants)) { + return false; + } + + foreach ($this->constants as $i => $constant) { + if (!$constant->equals($node->constants[$i])) { + return false; + } + } + + return $this->public === $node->public + && $this->private === $node->private + && $this->final === $node->final; + } + + /** + * @param mixed[] $properties + */ + 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] + #[Override] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'constants' => $this->constants, + 'public' => $this->public, + 'private' => $this->private, + 'final' => $this->final, + 'phpDoc' => $this->phpDoc, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index 4977a47643..58c3d17fd4 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -3,59 +3,37 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedClassNode implements ExportedNode, JsonSerializable +final class ExportedClassNode implements RootExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $abstract; - - private bool $final; - - private ?string $extends; - - /** @var string[] */ - private array $implements; - - /** @var string[] */ - private array $usedTraits; - - /** @var ExportedTraitUseAdaptation[] */ - private array $traitUseAdaptations; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $abstract - * @param bool $final - * @param string|null $extends * @param string[] $implements * @param string[] $usedTraits * @param ExportedTraitUseAdaptation[] $traitUseAdaptations + * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $abstract, - bool $final, - ?string $extends, - array $implements, - array $usedTraits, - array $traitUseAdaptations + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $abstract, + private bool $final, + private ?string $extends, + private array $implements, + private array $usedTraits, + private array $traitUseAdaptations, + private array $statements, + private array $attributes, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->abstract = $abstract; - $this->final = $final; - $this->extends = $extends; - $this->implements = $implements; - $this->usedTraits = $usedTraits; - $this->traitUseAdaptations = $traitUseAdaptations; } public function equals(ExportedNode $node): bool @@ -76,6 +54,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + if (count($this->traitUseAdaptations) !== count($node->traitUseAdaptations)) { return false; } @@ -87,6 +75,18 @@ public function equals(ExportedNode $node): bool } } + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + return $this->name === $node->name && $this->abstract === $node->abstract && $this->final === $node->final @@ -97,9 +97,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -109,13 +108,17 @@ public static function __set_state(array $properties): ExportedNode $properties['extends'], $properties['implements'], $properties['usedTraits'], - $properties['traitUseAdaptations'] + $properties['traitUseAdaptations'], + $properties['statements'], + $properties['attributes'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -129,15 +132,16 @@ public function jsonSerialize() 'implements' => $this->implements, 'usedTraits' => $this->usedTraits, 'traitUseAdaptations' => $this->traitUseAdaptations, + 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -149,11 +153,35 @@ public static function decode(array $data): ExportedNode $data['usedTraits'], array_map(static function (array $traitUseAdaptationData): ExportedTraitUseAdaptation { if ($traitUseAdaptationData['type'] !== ExportedTraitUseAdaptation::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedTraitUseAdaptation::decode($traitUseAdaptationData['data']); - }, $data['traitUseAdaptations']) + }, $data['traitUseAdaptations']), + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), + 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..eb77579c89 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php @@ -0,0 +1,80 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + return $this->name === $node->name + && $this->value === $node->value; + } + + /** + * @param mixed[] $properties + */ + 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] + #[Override] + 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..1378b5ea80 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -0,0 +1,150 @@ +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] + #[Override] + 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 index 6c4382b1b0..5c618302a5 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -3,42 +3,30 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedFunctionNode implements ExportedNode, JsonSerializable +final class ExportedFunctionNode implements RootExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $byRef; - - private ?string $returnType; - - /** @var ExportedParameterNode[] */ - private array $parameters; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $byRef - * @param string|null $returnType * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $byRef, - ?string $returnType, - array $parameters + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $byRef, + private ?string $returnType, + private array $parameters, + private array $attributes, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->byRef = $byRef; - $this->returnType = $returnType; - $this->parameters = $parameters; } public function equals(ExportedNode $node): bool @@ -70,6 +58,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->returnType === $node->returnType; @@ -77,22 +75,24 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], $properties['phpDoc'], $properties['byRef'], $properties['returnType'], - $properties['parameters'] + $properties['parameters'], + $properties['attributes'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -103,15 +103,15 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -120,11 +120,30 @@ public static function decode(array $data): ExportedNode $data['returnType'], array_map(static function (array $parameterData): ExportedParameterNode { if ($parameterData['type'] !== ExportedParameterNode::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedParameterNode::decode($parameterData['data']); - }, $data['parameters']) + }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } + /** + * @return self::TYPE_FUNCTION + */ + public function getType(): string + { + return self::TYPE_FUNCTION; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedInterfaceNode.php b/src/Dependency/ExportedNode/ExportedInterfaceNode.php index 0519efba99..c6f9aad58a 100644 --- a/src/Dependency/ExportedNode/ExportedInterfaceNode.php +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -3,28 +3,22 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedInterfaceNode implements ExportedNode, JsonSerializable +final class ExportedInterfaceNode implements RootExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - /** @var string[] */ - private array $extends; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc * @param string[] $extends + * @param ExportedNode[] $statements */ - public function __construct(string $name, ?ExportedPhpDocNode $phpDoc, array $extends) + public function __construct(private string $name, private ?ExportedPhpDocNode $phpDoc, private array $extends, private array $statements) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->extends = $extends; } public function equals(ExportedNode $node): bool @@ -45,26 +39,40 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + return $this->name === $node->name && $this->extends === $node->extends; } /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], $properties['phpDoc'], - $properties['extends'] + $properties['extends'], + $properties['statements'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -73,21 +81,39 @@ public function jsonSerialize() 'name' => $this->name, 'phpDoc' => $this->phpDoc, 'extends' => $this->extends, + 'statements' => $this->statements, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, - $data['extends'] + $data['extends'], + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), ); } + /** + * @return self::TYPE_INTERFACE + */ + public function getType(): string + { + return self::TYPE_INTERFACE; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index a59a40c25a..eaeda487c5 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -3,67 +3,34 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedMethodNode implements ExportedNode, JsonSerializable +final class ExportedMethodNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $byRef; - - private bool $public; - - private bool $private; - - private bool $abstract; - - private bool $final; - - private bool $static; - - private ?string $returnType; - - /** @var ExportedParameterNode[] */ - private array $parameters; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $byRef - * @param bool $public - * @param bool $private - * @param bool $abstract - * @param bool $final - * @param bool $static - * @param string|null $returnType * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $byRef, - bool $public, - bool $private, - bool $abstract, - bool $final, - bool $static, - ?string $returnType, - array $parameters + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $byRef, + private bool $public, + private bool $private, + private bool $abstract, + private bool $final, + private bool $static, + private ?string $returnType, + private array $parameters, + private array $attributes, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->byRef = $byRef; - $this->public = $public; - $this->private = $private; - $this->abstract = $abstract; - $this->final = $final; - $this->static = $static; - $this->returnType = $returnType; - $this->parameters = $parameters; } public function equals(ExportedNode $node): bool @@ -95,6 +62,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->public === $node->public @@ -107,9 +84,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -121,13 +97,16 @@ public static function __set_state(array $properties): ExportedNode $properties['final'], $properties['static'], $properties['returnType'], - $properties['parameters'] + $properties['parameters'], + $properties['attributes'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -143,15 +122,15 @@ public function jsonSerialize() 'static' => $this->static, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -165,10 +144,16 @@ public static function decode(array $data): ExportedNode $data['returnType'], array_map(static function (array $parameterData): ExportedParameterNode { if ($parameterData['type'] !== ExportedParameterNode::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedParameterNode::decode($parameterData['data']); - }, $data['parameters']) + }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index 5f171a442f..ab00eb5538 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -3,34 +3,28 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedParameterNode implements ExportedNode, JsonSerializable +final class ExportedParameterNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?string $type; - - private bool $byRef; - - private bool $variadic; - - private bool $hasDefault; - + /** + * @param ExportedAttributeNode[] $attributes + */ public function __construct( - string $name, - ?string $type, - bool $byRef, - bool $variadic, - bool $hasDefault + private string $name, + private ?string $type, + private bool $byRef, + private bool $variadic, + private bool $hasDefault, + private array $attributes, ) { - $this->name = $name; - $this->type = $type; - $this->byRef = $byRef; - $this->variadic = $variadic; - $this->hasDefault = $hasDefault; } public function equals(ExportedNode $node): bool @@ -39,6 +33,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->type === $node->type && $this->byRef === $node->byRef @@ -48,22 +52,24 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], $properties['type'], $properties['byRef'], $properties['variadic'], - $properties['hasDefault'] + $properties['hasDefault'], + $properties['attributes'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -74,22 +80,28 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'variadic' => $this->variadic, 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], $data['type'], $data['byRef'], $data['variadic'], - $data['hasDefault'] + $data['hasDefault'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedPhpDocNode.php b/src/Dependency/ExportedNode/ExportedPhpDocNode.php index e271b6b752..0ce56bbb54 100644 --- a/src/Dependency/ExportedNode/ExportedPhpDocNode.php +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -3,28 +3,19 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; -class ExportedPhpDocNode implements ExportedNode, JsonSerializable +final class ExportedPhpDocNode implements ExportedNode, JsonSerializable { - private string $phpDocString; - - private ?string $namespace; - - /** @var array alias(string) => fullName(string) */ - private array $uses; - /** - * @param string $phpDocString - * @param string|null $namespace - * @param array $uses + * @param array $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) */ - public function __construct(string $phpDocString, ?string $namespace, array $uses) + public function __construct(private string $phpDocString, private ?string $namespace, private array $uses, private array $constUses) { - $this->phpDocString = $phpDocString; - $this->namespace = $namespace; - $this->uses = $uses; } public function equals(ExportedNode $node): bool @@ -35,12 +26,15 @@ public function equals(ExportedNode $node): bool return $this->phpDocString === $node->phpDocString && $this->namespace === $node->namespace - && $this->uses === $node->uses; + && $this->uses === $node->uses + && $this->constUses === $node->constUses; } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -49,26 +43,25 @@ public function jsonSerialize() 'phpDocString' => $this->phpDocString, 'namespace' => $this->namespace, 'uses' => $this->uses, + 'constUses' => $this->constUses, ], ]; } /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { - return new self($properties['phpDocString'], $properties['namespace'], $properties['uses']); + return new self($properties['phpDocString'], $properties['namespace'], $properties['uses'], $properties['constUses'] ?? []); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { - return new self($data['phpDocString'], $data['namespace'], $data['uses']); + return new self($data['phpDocString'], $data['namespace'], $data['uses'], $data['constUses'] ?? []); } } diff --git a/src/Dependency/ExportedNode/ExportedPropertiesNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php new file mode 100644 index 0000000000..80b6abc674 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -0,0 +1,189 @@ +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; + } + } + + if (count($this->hooks) !== count($node->hooks)) { + return false; + } + + foreach ($this->hooks as $i => $hook) { + if (!$hook->equals($node->hooks[$i])) { + return false; + } + } + + return $this->type === $node->type + && $this->public === $node->public + && $this->private === $node->private + && $this->static === $node->static + && $this->readonly === $node->readonly + && $this->abstract === $node->abstract + && $this->final === $node->final + && $this->publicSet === $node->publicSet + && $this->protectedSet === $node->protectedSet + && $this->privateSet === $node->privateSet + && $this->virtual === $node->virtual; + } + + /** + * @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['abstract'], + $properties['final'], + $properties['publicSet'], + $properties['protectedSet'], + $properties['privateSet'], + $properties['virtual'], + $properties['attributes'], + $properties['hooks'], + ); + } + + /** + * @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'], + $data['abstract'], + $data['final'], + $data['publicSet'], + $data['protectedSet'], + $data['privateSet'], + $data['virtual'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), + array_map(static function (array $attributeData): ExportedPropertyHookNode { + if ($attributeData['type'] !== ExportedPropertyHookNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedPropertyHookNode::decode($attributeData['data']); + }, $data['hooks']), + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + #[Override] + 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, + 'abstract' => $this->abstract, + 'final' => $this->final, + 'publicSet' => $this->publicSet, + 'protectedSet' => $this->protectedSet, + 'privateSet' => $this->privateSet, + 'virtual' => $this->virtual, + 'attributes' => $this->attributes, + 'hooks' => $this->hooks, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedPropertyHookNode.php b/src/Dependency/ExportedNode/ExportedPropertyHookNode.php new file mode 100644 index 0000000000..17aba62694 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedPropertyHookNode.php @@ -0,0 +1,145 @@ +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->abstract === $node->abstract + && $this->final === $node->final + && $this->short === $node->short; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['byRef'], + $properties['abstract'], + $properties['final'], + $properties['short'], + $properties['parameters'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + #[Override] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + 'byRef' => $this->byRef, + 'abstract' => $this->abstract, + 'final' => $this->final, + 'short' => $this->short, + '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['abstract'], + $data['final'], + $data['short'], + 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/ExportedPropertyNode.php b/src/Dependency/ExportedNode/ExportedPropertyNode.php deleted file mode 100644 index b5bf696d47..0000000000 --- a/src/Dependency/ExportedNode/ExportedPropertyNode.php +++ /dev/null @@ -1,123 +0,0 @@ -name = $name; - $this->phpDoc = $phpDoc; - $this->type = $type; - $this->public = $public; - $this->private = $private; - $this->static = $static; - $this->readonly = $readonly; - } - - public function equals(ExportedNode $node): bool - { - if (!$node instanceof self) { - 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; - } - - return $this->name === $node->name - && $this->type === $node->type - && $this->public === $node->public - && $this->private === $node->private - && $this->static === $node->static - && $this->readonly === $node->readonly; - } - - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): ExportedNode - { - return new self( - $properties['name'], - $properties['phpDoc'], - $properties['type'], - $properties['public'], - $properties['private'], - $properties['static'], - $properties['readonly'] - ); - } - - /** - * @param mixed[] $data - * @return self - */ - public static function decode(array $data): ExportedNode - { - return new self( - $data['name'], - $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, - $data['type'], - $data['public'], - $data['private'], - $data['static'], - $data['readonly'] - ); - } - - /** - * @return mixed - */ - public function jsonSerialize() - { - return [ - 'type' => self::class, - 'data' => [ - 'name' => $this->name, - 'phpDoc' => $this->phpDoc, - 'type' => $this->type, - 'public' => $this->public, - 'private' => $this->private, - 'static' => $this->static, - 'readonly' => $this->readonly, - ], - ]; - } - -} diff --git a/src/Dependency/ExportedNode/ExportedTraitNode.php b/src/Dependency/ExportedNode/ExportedTraitNode.php index ed8a6fa5ef..1dfed9a9ca 100644 --- a/src/Dependency/ExportedNode/ExportedTraitNode.php +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -3,52 +3,164 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedTraitNode implements ExportedNode, JsonSerializable +final class ExportedTraitNode implements RootExportedNode, JsonSerializable { - private string $traitName; - - public function __construct(string $traitName) + /** + * @param string[] $usedTraits + * @param ExportedTraitUseAdaptation[] $traitUseAdaptations + * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes + */ + public function __construct( + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private array $usedTraits, + private array $traitUseAdaptations, + private array $statements, + private array $attributes, + ) { - $this->traitName = $traitName; } public function equals(ExportedNode $node): bool { - return false; - } + if (!$node instanceof self) { + return false; + } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): ExportedNode - { - return new self($properties['traitName']); + 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; + } + } + + 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->usedTraits === $node->usedTraits; } /** - * @param mixed[] $data - * @return self + * @param mixed[] $properties */ - public static function decode(array $data): ExportedNode + public static function __set_state(array $properties): self { - return new self($data['traitName']); + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['usedTraits'], + $properties['traitUseAdaptations'], + $properties['statements'], + $properties['attributes'], + ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ 'type' => self::class, 'data' => [ - 'traitName' => $this->traitName, + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + '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['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_TRAIT + */ + public function getType(): string + { + return self::TYPE_TRAIT; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php index 1de62fcf0a..3ddc6a18dd 100644 --- a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php +++ b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php @@ -3,64 +3,43 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; -class ExportedTraitUseAdaptation implements ExportedNode, JsonSerializable +final class ExportedTraitUseAdaptation implements ExportedNode, JsonSerializable { - private ?string $traitName; - - private string $method; - - private ?int $newModifier; - - private ?string $newName; - - /** @var string[]|null */ - private ?array $insteadOfs; - /** - * @param string|null $traitName - * @param string $method - * @param int|null $newModifier - * @param string|null $newName * @param string[]|null $insteadOfs */ private function __construct( - ?string $traitName, - string $method, - ?int $newModifier, - ?string $newName, - ?array $insteadOfs + private ?string $traitName, + private string $method, + private ?int $newModifier, + private ?string $newName, + private ?array $insteadOfs, ) { - $this->traitName = $traitName; - $this->method = $method; - $this->newModifier = $newModifier; - $this->newName = $newName; - $this->insteadOfs = $insteadOfs; } public static function createAlias( ?string $traitName, string $method, ?int $newModifier, - ?string $newName + ?string $newName, ): self { return new self($traitName, $method, $newModifier, $newName, null); } /** - * @param string|null $traitName - * @param string $method * @param string[] $insteadOfs - * @return self */ public static function createPrecedence( ?string $traitName, string $method, - array $insteadOfs + array $insteadOfs, ): self { return new self($traitName, $method, null, null, $insteadOfs); @@ -81,37 +60,37 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['traitName'], $properties['method'], $properties['newModifier'], $properties['newName'], - $properties['insteadOfs'] + $properties['insteadOfs'], ); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['traitName'], $data['method'], $data['newModifier'], $data['newName'], - $data['insteadOfs'] + $data['insteadOfs'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php index 8a4c6beb79..e12bf67b9c 100644 --- a/src/Dependency/ExportedNodeFetcher.php +++ b/src/Dependency/ExportedNodeFetcher.php @@ -3,27 +3,25 @@ namespace PHPStan\Dependency; use PhpParser\NodeTraverser; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; -class ExportedNodeFetcher +#[AutowiredService] +final class ExportedNodeFetcher { - private Parser $parser; - - private ExportedNodeVisitor $visitor; - public function __construct( - Parser $parser, - ExportedNodeVisitor $visitor + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + private Parser $parser, + private ExportedNodeVisitor $visitor, ) { - $this->parser = $parser; - $this->visitor = $visitor; } /** - * @param string $fileName - * @return ExportedNode[] + * @return RootExportedNode[] */ public function fetchNodes(string $fileName): array { @@ -31,9 +29,8 @@ public function fetchNodes(string $fileName): array $nodeTraverser->addVisitor($this->visitor); try { - /** @var \PhpParser\Node[] $ast */ $ast = $this->parser->parseFile($fileName); - } catch (\PHPStan\Parser\ParserErrorsException $e) { + } catch (ParserErrorsException) { return []; } $this->visitor->reset($fileName); diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 0353437cac..5a6afabe70 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -3,38 +3,49 @@ namespace PHPStan\Dependency; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Property; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Dependency\ExportedNode\ExportedAttributeNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; +use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; +use PHPStan\Dependency\ExportedNode\ExportedEnumCaseNode; +use PHPStan\Dependency\ExportedNode\ExportedEnumNode; use PHPStan\Dependency\ExportedNode\ExportedFunctionNode; use PHPStan\Dependency\ExportedNode\ExportedInterfaceNode; use PHPStan\Dependency\ExportedNode\ExportedMethodNode; use PHPStan\Dependency\ExportedNode\ExportedParameterNode; use PHPStan\Dependency\ExportedNode\ExportedPhpDocNode; -use PHPStan\Dependency\ExportedNode\ExportedPropertyNode; +use PHPStan\Dependency\ExportedNode\ExportedPropertiesNode; +use PHPStan\Dependency\ExportedNode\ExportedPropertyHookNode; use PHPStan\Dependency\ExportedNode\ExportedTraitNode; use PHPStan\Dependency\ExportedNode\ExportedTraitUseAdaptation; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\NodeTypePrinter; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function is_string; +use function sprintf; -class ExportedNodeResolver +#[AutowiredService] +final class ExportedNodeResolver { - private FileTypeMapper $fileTypeMapper; - - private Standard $printer; - - public function __construct(FileTypeMapper $fileTypeMapper, Standard $printer) + public function __construct( + private ReflectionProvider $reflectionProvider, + private FileTypeMapper $fileTypeMapper, + private ExprPrinter $exprPrinter, + ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->printer = $printer; } - public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode + public function resolve(string $fileName, Node $node): ?RootExportedNode { if ($node instanceof Class_ && isset($node->namespacedName)) { $docComment = $node->getDocComment(); @@ -67,7 +78,7 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, $className, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), $node->isAbstract(), $node->isFinal(), @@ -80,7 +91,7 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $adaptation->trait !== null ? $adaptation->trait->toString() : null, $adaptation->method->toString(), $adaptation->newModifier, - $adaptation->newName !== null ? $adaptation->newName->toString() : null + $adaptation->newName !== null ? $adaptation->newName->toString() : null, ); } @@ -88,21 +99,19 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode return ExportedTraitUseAdaptation::createPrecedence( $adaptation->trait !== null ? $adaptation->trait->toString() : null, $adaptation->method->toString(), - array_map(static function (Name $name): string { - return $name->toString(); - }, $adaptation->insteadof) + array_map(static fn (Name $name): string => $name->toString(), $adaptation->insteadof), ); } - throw new \PHPStan\ShouldNotHappenException(); - }, $adaptations) + throw new ShouldNotHappenException(); + }, $adaptations), + $this->exportClassStatements($node->stmts, $fileName, $className), + $this->exportAttributeNodes($node->attrGroups), ); } - if ($node instanceof \PhpParser\Node\Stmt\Interface_ && isset($node->namespacedName)) { - $extendsNames = array_map(static function (Name $name): string { - return (string) $name; - }, $node->extends); + if ($node instanceof Node\Stmt\Interface_ && isset($node->namespacedName)) { + $extendsNames = array_map(static fn (Name $name): string => (string) $name, $node->extends); $docComment = $node->getDocComment(); $interfaceName = $node->namespacedName->toString(); @@ -113,107 +122,84 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, $interfaceName, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), - $extendsNames + $extendsNames, + $this->exportClassStatements($node->stmts, $fileName, $interfaceName), ); } - if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { - return new ExportedTraitNode($node->namespacedName->toString()); - } - - if ($node instanceof ClassMethod) { - if ($node->isAbstract() || $node->isFinal() || !$node->isPrivate()) { - $methodName = $node->name->toString(); - $docComment = $node->getDocComment(); - $parentNode = $node->getAttribute('parent'); - $continue = ($parentNode instanceof Class_ || $parentNode instanceof Node\Stmt\Interface_) && isset($parentNode->namespacedName); - if (!$continue) { - return null; - } - - return new ExportedMethodNode( - $methodName, - $this->exportPhpDocNode( - $fileName, - $parentNode->namespacedName->toString(), - $methodName, - $docComment !== null ? $docComment->getText() : null - ), - $node->byRef, - $node->isPublic(), - $node->isPrivate(), - $node->isAbstract(), - $node->isFinal(), - $node->isStatic(), - $this->printType($node->returnType), - $this->exportParameterNodes($node->params) - ); - } - } - - if ($node instanceof Node\Stmt\PropertyProperty) { - $parentNode = $node->getAttribute('parent'); - if (!$parentNode instanceof Property) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Expected node type %s, %s occurred.', Property::class, is_object($parentNode) ? get_class($parentNode) : gettype($parentNode))); - } - if ($parentNode->isPrivate()) { - return null; - } + if ($node instanceof Node\Stmt\Enum_ && $node->namespacedName !== null) { + $implementsNames = array_map(static fn (Name $name): string => (string) $name, $node->implements); + $docComment = $node->getDocComment(); - $classNode = $parentNode->getAttribute('parent'); - if (!$classNode instanceof Class_ || !isset($classNode->namespacedName)) { - return null; + $enumName = $node->namespacedName->toString(); + $scalarType = null; + if ($node->scalarType !== null) { + $scalarType = $node->scalarType->toString(); } - $docComment = $parentNode->getDocComment(); - - return new ExportedPropertyNode( - $node->name->toString(), + return new ExportedEnumNode( + $enumName, + $scalarType, $this->exportPhpDocNode( $fileName, - $classNode->namespacedName->toString(), + $enumName, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), - $this->printType($parentNode->type), - $parentNode->isPublic(), - $parentNode->isPrivate(), - $parentNode->isStatic(), - $parentNode->isReadonly() + $implementsNames, + $this->exportClassStatements($node->stmts, $fileName, $enumName), + $this->exportAttributeNodes($node->attrGroups), ); } - if ($node instanceof Node\Const_) { - $parentNode = $node->getAttribute('parent'); - if (!$parentNode instanceof Node\Stmt\ClassConst) { - return null; - } - - if ($parentNode->isPrivate()) { - return null; - } - - $classNode = $parentNode->getAttribute('parent'); - if (!$classNode instanceof Class_ || !isset($classNode->namespacedName)) { - return null; + if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { + $docComment = $node->getDocComment(); + $usedTraits = []; + $adaptations = []; + foreach ($node->getTraitUses() as $traitUse) { + foreach ($traitUse->traits as $usedTraitName) { + $usedTraits[] = $usedTraitName->toString(); + } + foreach ($traitUse->adaptations as $adaptation) { + $adaptations[] = $adaptation; + } } - $docComment = $parentNode->getDocComment(); + $className = $node->namespacedName->toString(); - return new ExportedClassConstantNode( - $node->name->toString(), - $this->printer->prettyPrintExpr($node->value), - $parentNode->isPublic(), - $parentNode->isPrivate(), - $parentNode->isFinal(), + return new ExportedTraitNode( + $className, $this->exportPhpDocNode( $fileName, - $classNode->namespacedName->toString(), + $className, null, - $docComment !== null ? $docComment->getText() : null - ) + $docComment !== null ? $docComment->getText() : null, + ), + $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), ); } @@ -231,60 +217,18 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, null, $functionName, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), $node->byRef, - $this->printType($node->returnType), - $this->exportParameterNodes($node->params) + NodeTypePrinter::printType($node->returnType), + $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), ); } return null; } - /** - * @param Node\Identifier|Node\Name|Node\ComplexType|null $type - * @return string|null - */ - private function printType($type): ?string - { - if ($type === null) { - return null; - } - - if ($type instanceof Node\NullableType) { - return '?' . $this->printType($type->type); - } - - if ($type instanceof Node\UnionType) { - return implode('|', array_map(function ($innerType): string { - $printedType = $this->printType($innerType); - if ($printedType === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $printedType; - }, $type->types)); - } - - if ($type instanceof Node\IntersectionType) { - return implode('&', array_map(function ($innerType): string { - $printedType = $this->printType($innerType); - if ($printedType === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $printedType; - }, $type->types)); - } - - if ($type instanceof Node\Identifier || $type instanceof Name) { - return $type->toString(); - } - - throw new \PHPStan\ShouldNotHappenException(); - } - /** * @param Node\Param[] $params * @return ExportedParameterNode[] @@ -294,7 +238,7 @@ private function exportParameterNodes(array $params): array $nodes = []; foreach ($params as $param) { if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $type = $param->type; if ( @@ -312,10 +256,11 @@ private function exportParameterNodes(array $params): array } $nodes[] = new ExportedParameterNode( $param->var->name, - $this->printType($type), + NodeTypePrinter::printType($type), $param->byRef, $param->variadic, - $param->default !== null + $param->default !== null, + $this->exportAttributeNodes($param->attrGroups), ); } @@ -326,7 +271,7 @@ private function exportPhpDocNode( string $file, ?string $className, ?string $functionName, - ?string $text + ?string $text, ): ?ExportedPhpDocNode { if ($text === null) { @@ -338,7 +283,7 @@ private function exportPhpDocNode( $className, null, $functionName, - $text + $text, ); $nameScope = $resolvedPhpDocBlock->getNullableNameScope(); @@ -346,7 +291,203 @@ private function exportPhpDocNode( return null; } - return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses()); + 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(); + + $names = array_map(static fn (Node\PropertyItem $prop): string => $prop->name->toString(), $node->props); + $virtual = false; + if ($this->reflectionProvider->hasClass($namespacedName)) { + $classReflection = $this->reflectionProvider->getClass($namespacedName); + if ($classReflection->hasNativeProperty($names[0])) { + $virtual = $classReflection->getNativeProperty($names[0])->isVirtual()->yes(); + } + } + + return new ExportedPropertiesNode( + $names, + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + NodeTypePrinter::printType($node->type), + $node->isPublic(), + $node->isPrivate(), + $node->isStatic(), + $node->isReadonly(), + $node->isAbstract(), + $node->isFinal(), + $node->isPublicSet(), + $node->isProtectedSet(), + $node->isPrivateSet(), + $virtual, + $this->exportAttributeNodes($node->attrGroups), + $this->exportPropertyHooks($node->hooks, $fileName, $namespacedName), + ); + } + + 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; + } + + /** + * @param Node\PropertyHook[] $hooks + * @return ExportedPropertyHookNode[] + */ + private function exportPropertyHooks( + array $hooks, + string $fileName, + string $namespacedName, + ): array + { + $nodes = []; + foreach ($hooks as $hook) { + $docComment = $hook->getDocComment(); + $propertyName = $hook->getAttribute('propertyName'); + if ($propertyName === null) { + continue; + } + $nodes[] = new ExportedPropertyHookNode( + $hook->name->toString(), + $this->exportPhpDocNode( + $fileName, + $namespacedName, + sprintf('$%s::%s', $propertyName, $hook->name->toString()), + $docComment !== null ? $docComment->getText() : null, + ), + $hook->byRef, + $hook->body === null, + $hook->isFinal(), + $hook->body instanceof Expr, + $this->exportParameterNodes($hook->params), + $this->exportAttributeNodes($hook->attrGroups), + ); + } + + return $nodes; } } diff --git a/src/Dependency/ExportedNodeVisitor.php b/src/Dependency/ExportedNodeVisitor.php index bedb18c037..7cd4610809 100644 --- a/src/Dependency/ExportedNodeVisitor.php +++ b/src/Dependency/ExportedNodeVisitor.php @@ -2,28 +2,26 @@ namespace PHPStan\Dependency; +use Override; use PhpParser\Node; -use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; +use PHPStan\ShouldNotHappenException; -class ExportedNodeVisitor extends NodeVisitorAbstract +final class ExportedNodeVisitor extends NodeVisitorAbstract { - private ExportedNodeResolver $exportedNodeResolver; - private ?string $fileName = null; - /** @var ExportedNode[] */ + /** @var RootExportedNode[] */ private array $currentNodes = []; /** * ExportedNodeVisitor constructor. * - * @param ExportedNodeResolver $exportedNodeResolver */ - public function __construct(ExportedNodeResolver $exportedNodeResolver) + public function __construct(private ExportedNodeResolver $exportedNodeResolver) { - $this->exportedNodeResolver = $exportedNodeResolver; } public function reset(string $fileName): void @@ -33,17 +31,18 @@ public function reset(string $fileName): void } /** - * @return ExportedNode[] + * @return RootExportedNode[] */ public function getExportedNodes(): array { return $this->currentNodes; } + #[Override] public function enterNode(Node $node): ?int { if ($this->fileName === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $exportedNode = $this->exportedNodeResolver->resolve($this->fileName, $node); if ($exportedNode !== null) { @@ -55,7 +54,7 @@ public function enterNode(Node $node): ?int || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\Trait_ ) { - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php index b6fcb0ce51..ac30175b35 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -3,35 +3,25 @@ namespace PHPStan\Dependency; use PHPStan\File\FileHelper; -use PHPStan\Reflection\ReflectionWithFilename; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\FunctionReflection; +use function array_values; -class NodeDependencies +final class NodeDependencies { - private FileHelper $fileHelper; - - /** @var array */ - private array $reflections; - - private ?ExportedNode $exportedNode; - /** - * @param FileHelper $fileHelper - * @param array $reflections + * @param array $reflections */ public function __construct( - FileHelper $fileHelper, - array $reflections, - ?ExportedNode $exportedNode + private FileHelper $fileHelper, + private array $reflections, + private ?RootExportedNode $exportedNode, ) { - $this->fileHelper = $fileHelper; - $this->reflections = $reflections; - $this->exportedNode = $exportedNode; } /** - * @param string $currentFile * @param array $analysedFiles * @return string[] */ @@ -60,7 +50,7 @@ public function getFileDependencies(string $currentFile, array $analysedFiles): return array_values($dependencies); } - public function getExportedNode(): ?ExportedNode + public function getExportedNode(): ?RootExportedNode { return $this->exportedNode; } diff --git a/src/Dependency/RootExportedNode.php b/src/Dependency/RootExportedNode.php new file mode 100644 index 0000000000..9434c91337 --- /dev/null +++ b/src/Dependency/RootExportedNode.php @@ -0,0 +1,23 @@ + Expect::int()->nullable()->required(), + ]); + } + + #[Override] + public function loadConfiguration(): void + { + require_once __DIR__ . '/../../vendor/attributes.php'; + $builder = $this->getContainerBuilder(); + + $autowiredParameters = Attributes::findTargetMethodParameters(AutowiredParameter::class); + + foreach (Attributes::findTargetClasses(AutowiredService::class) as $class) { + $reflection = new ReflectionClass($class->name); + $attribute = $class->attribute; + + $definition = $builder->addDefinition($attribute->name) + ->setType($class->name) + ->setAutowired($attribute->as); + + if ($attribute->factory !== null) { + [$ref, $method] = explode('::', $attribute->factory); + $definition->setFactory(new Statement([new Reference(substr($ref, 1)), $method])); + } + + $this->processParameters($class->name, $definition, $autowiredParameters); + + foreach (ValidateServiceTagsExtension::INTERFACE_TAG_MAPPING as $interface => $tag) { + if (!$reflection->implementsInterface($interface)) { + continue; + } + + $definition->addTag($tag); + } + } + + foreach (Attributes::findTargetClasses(NonAutowiredService::class) as $class) { + $attribute = $class->attribute; + + $definition = $builder->addDefinition($attribute->name) + ->setType($class->name) + ->setAutowired(false); + + if ($attribute->factory !== null) { + [$ref, $method] = explode('::', $attribute->factory); + $definition->setFactory(new Statement([new Reference(substr($ref, 1)), $method])); + } + + $this->processParameters($class->name, $definition, $autowiredParameters); + } + + foreach (Attributes::findTargetClasses(GenerateFactory::class) as $class) { + $attribute = $class->attribute; + $definition = $builder->addFactoryDefinition(null) + ->setImplement($attribute->interface); + + $resultDefinition = $definition->getResultDefinition(); + $this->processParameters($class->name, $resultDefinition, $autowiredParameters); + } + + /** @var stdClass&object{level: int|null} $config */ + $config = $this->getConfig(); + if ($config->level === null) { + return; + } + + foreach (Attributes::findTargetClasses(RegisteredRule::class) as $class) { + $attribute = $class->attribute; + if ($attribute->level > $config->level) { + continue; + } + + $definition = $builder->addDefinition(null) + ->setFactory($class->name) + ->setAutowired($class->name) + ->addTag(LazyRegistry::RULE_TAG); + + $this->processParameters($class->name, $definition, $autowiredParameters); + } + + foreach (Attributes::findTargetClasses(RegisteredCollector::class) as $class) { + $attribute = $class->attribute; + if ($attribute->level > $config->level) { + continue; + } + + $definition = $builder->addDefinition(null) + ->setFactory($class->name) + ->setAutowired($class->name) + ->addTag(RegistryFactory::COLLECTOR_TAG); + + $this->processParameters($class->name, $definition, $autowiredParameters); + } + } + + /** + * @param class-string $className + * @param TargetMethodParameter[] $autowiredParameters + */ + private function processParameters(string $className, ServiceDefinition $definition, array $autowiredParameters): void + { + $builder = $this->getContainerBuilder(); + foreach ($autowiredParameters as $autowiredParameter) { + if (strtolower($autowiredParameter->method) !== '__construct') { + continue; + } + if (strtolower($autowiredParameter->class) !== strtolower($className)) { + continue; + } + $ref = $autowiredParameter->attribute->ref; + if ($ref === null) { + $argument = Helpers::expand( + '%' . Helpers::escape($autowiredParameter->name) . '%', + $builder->parameters, + ); + } elseif (Strings::match($ref, '#^@[\w\\\\]+$#D') !== null) { + $argument = new Reference(substr($ref, 1)); + } else { + $argument = Helpers::expand( + $ref, + $builder->parameters, + ); + } + $definition->setArgument($autowiredParameter->name, $argument); + } + } + +} diff --git a/src/DependencyInjection/AutowiredParameter.php b/src/DependencyInjection/AutowiredParameter.php new file mode 100644 index 0000000000..d8547af2f8 --- /dev/null +++ b/src/DependencyInjection/AutowiredParameter.php @@ -0,0 +1,24 @@ +|class-string $as + */ + public function __construct( + public ?string $name = null, + public ?string $factory = null, + public bool|array|string $as = true, + ) + { + } + +} diff --git a/src/DependencyInjection/BleedingEdgeToggle.php b/src/DependencyInjection/BleedingEdgeToggle.php index f510a64c8d..98e9a52d41 100644 --- a/src/DependencyInjection/BleedingEdgeToggle.php +++ b/src/DependencyInjection/BleedingEdgeToggle.php @@ -2,12 +2,12 @@ namespace PHPStan\DependencyInjection; -class BleedingEdgeToggle +final class BleedingEdgeToggle { private static bool $bleedingEdge = false; - public static function isBleedingEdge(): bool + public static function isBleedingEdge(): bool // @phpstan-ignore shipmonk.deadMethod (kept for future use) { return self::$bleedingEdge; } diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 4808fcc85e..04272124a1 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -3,33 +3,31 @@ namespace PHPStan\DependencyInjection; use Nette; +use Nette\DI\CompilerExtension; use Nette\Schema\Expect; -use PHPStan\Analyser\TypeSpecifierFactory; -use PHPStan\Broker\BrokerFactory; -use PHPStan\PhpDoc\TypeNodeResolverExtension; -use PHPStan\Rules\RegistryFactory; +use Override; +use PHPStan\ShouldNotHappenException; +use function array_fill_keys; +use function array_reduce; +use function array_values; +use function count; +use function is_array; +use function sprintf; -class ConditionalTagsExtension extends \Nette\DI\CompilerExtension +final class ConditionalTagsExtension extends CompilerExtension { + #[Override] public function getConfigSchema(): Nette\Schema\Schema { - $bool = Expect::bool(); - return Expect::arrayOf(Expect::structure([ - BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG => $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::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - RegistryFactory::RULE_TAG => $bool, - TypeNodeResolverExtension::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, - ])->min(1)); + $tags = array_values(ValidateServiceTagsExtension::INTERFACE_TAG_MAPPING); + + return Expect::arrayOf(Expect::structure( + array_fill_keys($tags, Expect::anyOf(Expect::bool(), Expect::listOf(Expect::bool()))), + )->min(1)); } + #[Override] public function beforeCompile(): void { /** @var mixed[] $config */ @@ -39,10 +37,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 1da912305f..1c918448ab 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -2,29 +2,65 @@ namespace PHPStan\DependencyInjection; +use DirectoryIterator; use Nette\DI\Config\Loader; +use Nette\DI\Container as OriginalNetteContainer; use Nette\DI\ContainerLoader; +use Override; +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(); } + #[Override] protected function createLoader(): Loader { return $this->loaderFactory->createLoader(); } + /** + * @param string[] $allConfigFiles + */ + public function setAllConfigFiles(array $allConfigFiles): void + { + $this->allConfigFiles = $allConfigFiles; + } + /** * @return mixed[] */ + #[Override] protected function getDefaultParameters(): array { return []; @@ -35,17 +71,163 @@ public function getContainerCacheDirectory(): string return $this->getCacheDirectory() . '/nette.configurator'; } + #[Override] public function loadContainer(): string { $loader = new ContainerLoader( $this->getContainerCacheDirectory(), - $this->parameters['debugMode'] + $this->staticParameters['debugMode'], ); - return $loader->load( + $attributesPhp = __DIR__ . '/../../vendor/attributes.php'; + + $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, + is_file($attributesPhp) ? sha1_file($attributesPhp) : 'attributes-missing', + 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'); + } + } + + #[Override] + 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 024b6e4cc9..005f72b3eb 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -9,25 +9,26 @@ interface Container public function hasService(string $serviceName): bool; /** - * @param string $serviceName * @return mixed + * @throws MissingServiceException */ public function getService(string $serviceName); /** - * @param string $className - * @return mixed + * @template T of object + * @param class-string $className + * @return T + * @throws MissingServiceException */ public function getByType(string $className); /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array; /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array; @@ -40,9 +41,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 2cf9f3fe48..379b1fb741 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -2,35 +2,76 @@ 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 PhpParser\Parser; use PHPStan\BetterReflection\BetterReflection; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; -use PHPStan\Broker\Broker; +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 Symfony\Component\Finder\Finder; -use function sys_get_temp_dir; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; +use function array_diff_key; +use function array_intersect; +use function array_key_exists; +use function array_keys; +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 implode; +use function ini_get; +use function is_array; +use function is_file; +use function is_readable; +use function is_string; +use function spl_object_id; +use function sprintf; +use function str_ends_with; +use function substr; -/** @api */ -class ContainerFactory +/** + * @api + */ +final class ContainerFactory { - private string $currentWorkingDirectory; - private FileHelper $fileHelper; private string $rootDirectory; private string $configDirectory; + private static ?int $lastInitializedContainerId = null; + + private bool $journalContainer = false; + /** @api */ - public function __construct(string $currentWorkingDirectory) + public function __construct(private string $currentWorkingDirectory) { - $this->currentWorkingDirectory = $currentWorkingDirectory; $this->fileHelper = new FileHelper($currentWorkingDirectory); $rootDir = __DIR__ . '/../..'; @@ -45,18 +86,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 $usedLevel - * @param string|null $generateBaselineFile - * @param string|null $cliAutoloadFile - * @param string|null $singleReflectionFile - * @param string|null $singleReflectionInsteadOfFile - * @return \PHPStan\DependencyInjection\Container */ public function create( string $tempDirectory, @@ -68,18 +107,28 @@ public function create( ?string $generateBaselineFile = null, ?string $cliAutoloadFile = null, ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null + ?string $singleReflectionInsteadOfFile = null, ): Container { + [$allConfigFiles, $projectConfig] = $this->detectDuplicateIncludedFiles( + array_merge([__DIR__ . '/../../conf/parametersSchema.neon'], $additionalConfigFiles), + [ + 'rootDir' => $this->rootDirectory, + 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), + ], + ); + $configurator = new Configurator(new LoaderFactory( $this->fileHelper, $this->rootDirectory, $this->currentWorkingDirectory, - $generateBaselineFile - )); + $generateBaselineFile, + $projectConfig['expandRelativePaths'], + ), $this->journalContainer); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, - 'extensions' => \Nette\DI\Extensions\ExtensionsExtension::class, + 'extensions' => ExtensionsExtension::class, ]; $configurator->setDebugMode(true); $configurator->setTempDirectory($tempDirectory); @@ -89,91 +138,292 @@ public function create( 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', 'tmpDir' => $tempDirectory, 'additionalConfigFiles' => $additionalConfigFiles, - 'analysedPaths' => $analysedPaths, + 'allConfigFiles' => $allConfigFiles, 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, - 'analysedPathsFromConfig' => $analysedPathsFromConfig, 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, - 'fixerTmpDir' => sys_get_temp_dir() . '/phpstan-fixer', + 'env' => getenv(), ]); $configurator->addDynamicParameters([ 'singleReflectionFile' => $singleReflectionFile, 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, + 'analysedPaths' => $analysedPaths, + 'analysedPathsFromConfig' => $analysedPathsFromConfig, ]); $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'); - BetterReflection::$phpVersion = $container->getByType(PhpVersion::class)->getVersionId(); + /** @var Parser $phpParser */ + $phpParser = $container->getService('phpParserDecorator'); BetterReflection::populate( - $container->getService('betterReflectionSourceLocator'), // @phpstan-ignore-line - $container->getService('betterReflectionClassReflector'), // @phpstan-ignore-line - $container->getService('betterReflectionFunctionReflector'), // @phpstan-ignore-line - $container->getService('betterReflectionConstantReflector'), // @phpstan-ignore-line - $container->getService('phpParserDecorator'), // @phpstan-ignore-line - $container->getByType(PhpStormStubsSourceStubber::class) + $container->getByType(PhpVersion::class)->getVersionId(), + $sourceLocator, + $reflector, + $phpParser, + $container->getByType(PhpStormStubsSourceStubber::class), + $container->getByType(Printer::class), ); - /** @var Broker $broker */ - $broker = $container->getByType(Broker::class); - Broker::registerInstance($broker); ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); + PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class)); + ObjectType::resetCaches(); $container->getService('typeSpecifier'); - BleedingEdgeToggle::setBleedingEdge($container->parameters['featureToggles']['bleedingEdge']); + BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); + } - return $container->getByType(Container::class); + public function getCurrentWorkingDirectory(): string + { + return $this->currentWorkingDirectory; } - public function clearOldContainers(string $tempDirectory): void + public function getRootDirectory(): string { - $configurator = new Configurator(new LoaderFactory( - $this->fileHelper, - $this->rootDirectory, - $this->currentWorkingDirectory, - null - )); - $configurator->setDebugMode(true); - $configurator->setTempDirectory($tempDirectory); + return $this->rootDirectory; + } - $finder = new Finder(); - $finder->name('Container_*')->in($configurator->getContainerCacheDirectory()); - $twoDaysAgo = time() - 24 * 60 * 60 * 2; + public function getConfigDirectory(): string + { + return $this->configDirectory; + } - foreach ($finder as $containerFile) { - $path = $containerFile->getRealPath(); - if ($path === false) { - continue; + /** + * @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); } - if ($containerFile->getATime() > $twoDaysAgo) { - continue; + } + + 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']) + ) { + $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.'); } - if ($containerFile->getCTime() > $twoDaysAgo) { + } + + foreach ($parameters['ignoreErrors'] ?? [] as $ignoreError) { + if (is_string($ignoreError)) { continue; } - @unlink($path); + $atLeastOneOf = ['message', 'messages', 'rawMessage', 'identifier', 'identifiers', 'path', 'paths']; + if (array_intersect($atLeastOneOf, array_keys($ignoreError)) === []) { + throw new InvalidIgnoredErrorException('An ignoreErrors entry must contain at least one of the following fields: ' . implode(', ', $atLeastOneOf) . '.'); + } + + foreach ([ + ['message', 'messages'], + ['rawMessage', 'message'], + ['rawMessage', 'messages'], + ['identifier', 'identifiers'], + ['path', 'paths'], + ] as [$field1, $field2]) { + if (array_key_exists($field1, $ignoreError) && array_key_exists($field2, $ignoreError)) { + throw new InvalidIgnoredErrorException(sprintf('An ignoreErrors entry cannot contain both %s and %s fields.', $field1, $field2)); + } + } + + if (array_key_exists('count', $ignoreError) && !array_key_exists('path', $ignoreError)) { + throw new InvalidIgnoredErrorException('An ignoreErrors entry with count field must also contain path field.'); + } } } - public function getCurrentWorkingDirectory(): string + /** + * @param Statement[] $statements + */ + private function processSchema(array $statements, bool $required = true): Schema { - return $this->currentWorkingDirectory; - } + if (count($statements) === 0) { + throw new ShouldNotHappenException(); + } - public function getRootDirectory(): string - { - return $this->rootDirectory; + $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; } - public function getConfigDirectory(): string + /** + * @param mixed $argument + * @return mixed + */ + private function processArgument($argument, bool $required = true) { - return $this->configDirectory; + 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 ada4f5f921..ffafdd548e 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -2,68 +2,54 @@ 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; - - private string $usedLevel; - - private ?string $generateBaselineFile; +#[AutowiredService] +final class DerivativeContainerFactory +{ /** - * @param string $currentWorkingDirectory - * @param string $tempDirectory * @param string[] $additionalConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string $usedLevel */ public function __construct( - string $currentWorkingDirectory, - string $tempDirectory, - array $additionalConfigFiles, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $analysedPathsFromConfig, - string $usedLevel, - ?string $generateBaselineFile + #[AutowiredParameter] + private string $currentWorkingDirectory, + #[AutowiredParameter(ref: '%tempDir%')] + private string $tempDirectory, + #[AutowiredParameter] + private array $additionalConfigFiles, + #[AutowiredParameter] + private array $analysedPaths, + #[AutowiredParameter] + private array $composerAutoloaderProjectPaths, + #[AutowiredParameter] + private array $analysedPathsFromConfig, + #[AutowiredParameter] + private string $usedLevel, + #[AutowiredParameter] + private ?string $generateBaselineFile, + #[AutowiredParameter] + private ?string $cliAutoloadFile, + #[AutowiredParameter] + private ?string $singleReflectionFile, + #[AutowiredParameter] + private ?string $singleReflectionInsteadOfFile, ) { - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->tempDirectory = $tempDirectory; - $this->additionalConfigFiles = $additionalConfigFiles; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; - $this->usedLevel = $usedLevel; - $this->generateBaselineFile = $generateBaselineFile; } /** * @param string[] $additionalConfigFiles - * @return \PHPStan\DependencyInjection\Container */ public function create(array $additionalConfigFiles): Container { $containerFactory = new ContainerFactory( - $this->currentWorkingDirectory + $this->currentWorkingDirectory, ); + $containerFactory->setJournalContainer(); return $containerFactory->create( $this->tempDirectory, @@ -72,7 +58,10 @@ public function create(array $additionalConfigFiles): Container $this->composerAutoloaderProjectPaths, $this->analysedPathsFromConfig, $this->usedLevel, - $this->generateBaselineFile + $this->generateBaselineFile, + $this->cliAutoloadFile, + $this->singleReflectionFile, + $this->singleReflectionInsteadOfFile, ); } diff --git a/src/DependencyInjection/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/ExpandRelativePathExtension.php b/src/DependencyInjection/ExpandRelativePathExtension.php new file mode 100644 index 0000000000..2e664839b4 --- /dev/null +++ b/src/DependencyInjection/ExpandRelativePathExtension.php @@ -0,0 +1,19 @@ +, 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/InvalidIgnoredErrorException.php b/src/DependencyInjection/InvalidIgnoredErrorException.php new file mode 100644 index 0000000000..c553301519 --- /dev/null +++ b/src/DependencyInjection/InvalidIgnoredErrorException.php @@ -0,0 +1,10 @@ +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 @@ + $expandRelativePaths + */ public function __construct( - FileHelper $fileHelper, - string $rootDir, - string $currentWorkingDirectory, - ?string $generateBaselineFile + private FileHelper $fileHelper, + private string $rootDir, + private string $currentWorkingDirectory, + private ?string $generateBaselineFile, + private array $expandRelativePaths, ) { - $this->fileHelper = $fileHelper; - $this->rootDir = $rootDir; - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->generateBaselineFile = $generateBaselineFile; } public function createLoader(): Loader { + $neonAdapter = new NeonAdapter($this->expandRelativePaths); + $loader = new NeonLoader($this->fileHelper, $this->generateBaselineFile); - $loader->addAdapter('dist', NeonAdapter::class); - $loader->addAdapter('neon', NeonAdapter::class); + $loader->addAdapter('dist', $neonAdapter); + $loader->addAdapter('neon', $neonAdapter); $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..dae8456aba 100644 --- a/src/DependencyInjection/MemoizingContainer.php +++ b/src/DependencyInjection/MemoizingContainer.php @@ -2,17 +2,20 @@ namespace PHPStan\DependencyInjection; -class MemoizingContainer implements Container -{ +use function array_key_exists; - private Container $originalContainer; +#[AutowiredService(as: Container::class)] +final class MemoizingContainer implements Container +{ /** @var array */ private array $servicesByType = []; - public function __construct(Container $originalContainer) + public function __construct( + #[AutowiredParameter(ref: '@PHPStan\DependencyInjection\Nette\NetteContainer')] + private Container $originalContainer, + ) { - $this->originalContainer = $originalContainer; } public function hasService(string $serviceName): bool @@ -20,19 +23,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 +60,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/MissingImplementedInterfaceInServiceWithTagException.php b/src/DependencyInjection/MissingImplementedInterfaceInServiceWithTagException.php new file mode 100644 index 0000000000..2bfc4ac2ad --- /dev/null +++ b/src/DependencyInjection/MissingImplementedInterfaceInServiceWithTagException.php @@ -0,0 +1,16 @@ + $expandRelativePaths + */ + public function __construct(private array $expandRelativePaths) + { + } + + /** * @return mixed[] */ + #[Override] public function load(string $file): array { $contents = FileReader::read($file); try { return $this->process((array) Neon::decode($contents), '', $file); - } catch (\Nette\Neon\Exception $e) { - throw new \Nette\Neon\Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); + } catch (Exception $e) { + throw new Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); } } @@ -45,12 +68,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,42 +100,33 @@ 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][paths][]', - '[parameters][excludes_analyse][]', - '[parameters][excludePaths][]', - '[parameters][excludePaths][analyse][]', - '[parameters][excludePaths][analyseAndScan][]', - '[parameters][ignoreErrors][][paths][]', - '[parameters][ignoreErrors][][path]', - '[parameters][bootstrapFiles][]', - '[parameters][scanFiles][]', - '[parameters][scanDirectories][]', - '[parameters][tmpDir]', - '[parameters][memoryLimitFile]', - '[parameters][benchmarkFile]', - '[parameters][stubFiles][]', - '[parameters][symfony][console_application_loader]', - '[parameters][symfony][container_xml_path]', - '[parameters][doctrine][objectManagerLoader]', - ], true) && is_string($val) && strpos($val, '%') === false && strpos($val, '*') !== 0) { + if (in_array($keyToResolve, $this->expandRelativePaths, true) && is_string($val) && !str_contains($val, '%') && !str_starts_with($val, '*')) { $fileHelper = $this->createFileHelperByFile($file); $val = $fileHelper->normalizePath($fileHelper->absolutizePath($val)); } @@ -123,59 +144,6 @@ public function process(array $arr, string $fileKey, string $file): array return $res; } - /** - * @param mixed[] $data - * @return string - */ - public function dump(array $data): string - { - array_walk_recursive( - $data, - static function (&$val): void { - if (!($val instanceof Statement)) { - return; - } - - $val = self::statementToEntity($val); - } - ); - return "# generated by Nette\n\n" . Neon::encode($data, Neon::BLOCK); - } - - private static function statementToEntity(Statement $val): Entity - { - array_walk_recursive( - $val->arguments, - static function (&$val): void { - if ($val instanceof Statement) { - $val = self::statementToEntity($val); - } elseif ($val instanceof Reference) { - $val = '@' . $val->getValue(); - } - } - ); - - $entity = $val->getEntity(); - if ($entity instanceof Reference) { - $entity = '@' . $entity->getValue(); - } elseif (is_array($entity)) { - if ($entity[0] instanceof Statement) { - return new Entity( - Neon::CHAIN, - [ - self::statementToEntity($entity[0]), - new Entity('::' . $entity[1], $val->arguments), - ] - ); - } elseif ($entity[0] instanceof Reference) { - $entity = '@' . $entity[0]->getValue() . '::' . $entity[1]; - } elseif (is_string($entity[0])) { - $entity = $entity[0] . '::' . $entity[1]; - } - } - return new Entity($entity, $val->arguments); - } - private function createFileHelperByFile(string $file): FileHelper { $dir = dirname($file); diff --git a/src/DependencyInjection/NeonLoader.php b/src/DependencyInjection/NeonLoader.php index cee5eb5e1e..6664157fcf 100644 --- a/src/DependencyInjection/NeonLoader.php +++ b/src/DependencyInjection/NeonLoader.php @@ -2,29 +2,24 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\Config\Loader; +use Override; 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[] */ + #[Override] public function load(string $file, ?bool $merge = true): array { if ($this->generateBaselineFile === null) { diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 5e57025649..842c76d159 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -2,19 +2,23 @@ namespace PHPStan\DependencyInjection\Nette; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\MissingServiceException; +use PHPStan\DependencyInjection\ParameterNotFoundException; +use function array_key_exists; +use function array_keys; +use function array_map; /** * @internal */ -class NetteContainer implements Container +#[AutowiredService(as: NetteContainer::class)] +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,25 +27,33 @@ public function hasService(string $serviceName): bool } /** - * @param string $serviceName * @return mixed */ public function getService(string $serviceName) { - return $this->container->getService($serviceName); + try { + return $this->container->getService($serviceName); + } catch (\Nette\DI\MissingServiceException $e) { + throw new MissingServiceException($e->getMessage(), previous: $e); + } } /** - * @param string $className - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { - return $this->container->getByType($className); + try { + return $this->container->getByType($className); + } catch (\Nette\DI\MissingServiceException $e) { + throw new MissingServiceException($e->getMessage(), previous: $e); + } } /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array @@ -50,7 +62,6 @@ public function findServiceNamesByType(string $className): array } /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array @@ -63,25 +74,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 +100,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/NonAutowiredService.php b/src/DependencyInjection/NonAutowiredService.php new file mode 100644 index 0000000000..d989197502 --- /dev/null +++ b/src/DependencyInjection/NonAutowiredService.php @@ -0,0 +1,24 @@ +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..05cb4df32c 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -2,36 +2,49 @@ namespace PHPStan\DependencyInjection\Reflection; -use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\AutowiredService; +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; - -class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider +use PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension; +use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; +use function array_merge; + +#[AutowiredService(as: ClassReflectionExtensionRegistryProvider::class)] +final class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { - private \PHPStan\DependencyInjection\Container $container; - - private ?\PHPStan\Reflection\ClassReflectionExtensionRegistry $registry = null; + private ?ClassReflectionExtensionRegistry $registry = null; - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): ClassReflectionExtensionRegistry { if ($this->registry === null) { - $phpClassReflectionExtension = $this->container->getByType(PhpClassReflectionExtension::class); $annotationsMethodsClassReflectionExtension = $this->container->getByType(AnnotationsMethodsClassReflectionExtension::class); $annotationsPropertiesClassReflectionExtension = $this->container->getByType(AnnotationsPropertiesClassReflectionExtension::class); + $mixinMethodsClassReflectionExtension = $this->container->getByType(MixinMethodsClassReflectionExtension::class); + $mixinPropertiesClassReflectionExtension = $this->container->getByType(MixinPropertiesClassReflectionExtension::class); + $soapClientMethodsClassReflectionExtension = $this->container->getByType(SoapClientMethodsClassReflectionExtension::class); + $universalObjectCratesClassReflectionExtension = $this->container->getByType(UniversalObjectCratesClassReflectionExtension::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($this->container->getServicesByTag(BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension, $mixinPropertiesClassReflectionExtension, $universalObjectCratesClassReflectionExtension]), + array_merge($this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension, $mixinMethodsClassReflectionExtension, $soapClientMethodsClassReflectionExtension]), + $this->container->getServicesByTag(BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG), + $this->container->getByType(RequireExtendsPropertiesClassReflectionExtension::class), + $this->container->getByType(RequireExtendsMethodsClassReflectionExtension::class), + $this->container->getByType(PhpClassReflectionExtension::class), ); } diff --git a/src/DependencyInjection/RegisteredCollector.php b/src/DependencyInjection/RegisteredCollector.php new file mode 100644 index 0000000000..42af140bfc --- /dev/null +++ b/src/DependencyInjection/RegisteredCollector.php @@ -0,0 +1,21 @@ + $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/ExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..5eb6180ca0 --- /dev/null +++ b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,12 @@ +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) - ); - } - - return $this->registry; + return $this->registry ??= new DynamicReturnTypeExtensionRegistry( + $this->container->getByType(ReflectionProvider::class), + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), + ); } } diff --git a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php index ef7885034c..1a56f43268 100644 --- a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php @@ -2,20 +2,19 @@ namespace PHPStan\DependencyInjection\Type; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider +#[AutowiredService(as: DynamicThrowTypeExtensionProvider::class)] +final class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider { public const FUNCTION_TAG = 'phpstan.dynamicFunctionThrowTypeExtension'; public const METHOD_TAG = 'phpstan.dynamicMethodThrowTypeExtension'; public const STATIC_METHOD_TAG = 'phpstan.dynamicStaticMethodThrowTypeExtension'; - private Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getDynamicFunctionThrowTypeExtensions(): array diff --git a/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..6efc5fcb80 --- /dev/null +++ b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,27 @@ +registry ??= new ExpressionTypeResolverExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG), + ); + } + +} diff --git a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php index 3f9f0ca376..ead76923a3 100644 --- a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -2,32 +2,26 @@ namespace PHPStan\DependencyInjection\Type; -use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Container; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; -class LazyOperatorTypeSpecifyingExtensionRegistryProvider implements OperatorTypeSpecifyingExtensionRegistryProvider +#[AutowiredService(as: OperatorTypeSpecifyingExtensionRegistryProvider::class)] +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) - ); - } - - return $this->registry; + return $this->registry ??= new OperatorTypeSpecifyingExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG), + ); } } diff --git a/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php new file mode 100644 index 0000000000..915fd3d621 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php @@ -0,0 +1,47 @@ +functionExtensions ??= $this->container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterClosureThisExtensions(): array + { + return $this->methodExtensions ??= $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterClosureThisExtensions(): array + { + return $this->staticMethodExtensions ??= $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php new file mode 100644 index 0000000000..ecc30869f5 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php @@ -0,0 +1,35 @@ +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..113eea7b29 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php @@ -0,0 +1,35 @@ +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/ParameterClosureThisExtensionProvider.php b/src/DependencyInjection/Type/ParameterClosureThisExtensionProvider.php new file mode 100644 index 0000000000..9528a0a764 --- /dev/null +++ b/src/DependencyInjection/Type/ParameterClosureThisExtensionProvider.php @@ -0,0 +1,27 @@ +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..2732bbdb05 --- /dev/null +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -0,0 +1,254 @@ +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); + + try { + $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + } catch (MissingStaticAccessorInstanceException) { + $originalReflectionProvider = null; + } + + try { + $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); + } catch (MissingStaticAccessorInstanceException) { + $originalPhpVersion = null; + } + + ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); + PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); + + try { + $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); + $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory, null); + + $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); + } finally { + if ($originalReflectionProvider !== null) { + ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + } + if ($originalPhpVersion !== null) { + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); + } + ObjectType::resetCaches(); + } + } + + 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/DependencyInjection/ValidateServiceTagsExtension.php b/src/DependencyInjection/ValidateServiceTagsExtension.php new file mode 100644 index 0000000000..0180baae09 --- /dev/null +++ b/src/DependencyInjection/ValidateServiceTagsExtension.php @@ -0,0 +1,155 @@ + BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG, + MethodsClassReflectionExtension::class => BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG, + AllowedSubTypesClassReflectionExtension::class => BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG, + DynamicMethodReturnTypeExtension::class => BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG, + DynamicStaticMethodReturnTypeExtension::class => BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG, + DynamicFunctionReturnTypeExtension::class => BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG, + OperatorTypeSpecifyingExtension::class => BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG, + ExpressionTypeResolverExtension::class => BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG, + TypeNodeResolverExtension::class => TypeNodeResolverExtension::EXTENSION_TAG, + Rule::class => LazyRegistry::RULE_TAG, + StubFilesExtension::class => StubFilesExtension::EXTENSION_TAG, + AlwaysUsedClassConstantsExtension::class => AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG, + AlwaysUsedMethodExtension::class => AlwaysUsedMethodExtensionProvider::EXTENSION_TAG, + ReadWritePropertiesExtension::class => ReadWritePropertiesExtensionProvider::EXTENSION_TAG, + FunctionTypeSpecifyingExtension::class => TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG, + MethodTypeSpecifyingExtension::class => TypeSpecifierFactory::METHOD_TYPE_SPECIFYING_EXTENSION_TAG, + StaticMethodTypeSpecifyingExtension::class => TypeSpecifierFactory::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG, + DynamicFunctionThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG, + DynamicMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::METHOD_TAG, + DynamicStaticMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG, + FunctionParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::FUNCTION_TAG, + MethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::METHOD_TAG, + StaticMethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::STATIC_METHOD_TAG, + FunctionParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG, + MethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::METHOD_TAG, + StaticMethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG, + FunctionParameterOutTypeExtension::class => LazyParameterOutTypeExtensionProvider::FUNCTION_TAG, + MethodParameterOutTypeExtension::class => LazyParameterOutTypeExtensionProvider::METHOD_TAG, + StaticMethodParameterOutTypeExtension::class => LazyParameterOutTypeExtensionProvider::STATIC_METHOD_TAG, + ResultCacheMetaExtension::class => ResultCacheMetaExtension::EXTENSION_TAG, + ClassConstantDeprecationExtension::class => ClassConstantDeprecationExtension::CLASS_CONSTANT_EXTENSION_TAG, + ClassDeprecationExtension::class => ClassDeprecationExtension::CLASS_EXTENSION_TAG, + EnumCaseDeprecationExtension::class => EnumCaseDeprecationExtension::ENUM_CASE_EXTENSION_TAG, + FunctionDeprecationExtension::class => FunctionDeprecationExtension::FUNCTION_EXTENSION_TAG, + MethodDeprecationExtension::class => MethodDeprecationExtension::METHOD_EXTENSION_TAG, + PropertyDeprecationExtension::class => PropertyDeprecationExtension::PROPERTY_EXTENSION_TAG, + RestrictedMethodUsageExtension::class => RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG, + RestrictedClassNameUsageExtension::class => RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG, + RestrictedFunctionUsageExtension::class => RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG, + RestrictedPropertyUsageExtension::class => RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG, + RestrictedClassConstantUsageExtension::class => RestrictedClassConstantUsageExtension::CLASS_CONSTANT_EXTENSION_TAG, + NodeVisitor::class => RichParser::VISITOR_SERVICE_TAG, + Collector::class => CollectorRegistryFactory::COLLECTOR_TAG, + DiagnoseExtension::class => DiagnoseExtension::EXTENSION_TAG, + ]; + + /** + * @throws MissingImplementedInterfaceInServiceWithTagException + */ + #[Override] + public function beforeCompile(): void + { + $builder = $this->getContainerBuilder(); + $mappingCount = count(self::INTERFACE_TAG_MAPPING); + $flippedMapping = array_flip(self::INTERFACE_TAG_MAPPING); + + if (count($flippedMapping) !== $mappingCount) { // @phpstan-ignore notIdentical.alwaysFalse + throw new ShouldNotHappenException('A tag is mapped to multiple interfaces'); + } + + foreach ($builder->getDefinitions() as $definition) { + /** @var class-string|null $className */ + $className = $definition->getType(); + if ($className === null) { + continue; + } + $reflection = new ReflectionClass($className); + foreach (array_keys($definition->getTags()) as $tag) { + if (!array_key_exists($tag, $flippedMapping)) { + continue; + } + + if ($reflection->implementsInterface($flippedMapping[$tag])) { + continue; + } + + throw new MissingImplementedInterfaceInServiceWithTagException($className, $tag, $flippedMapping[$tag]); + } + } + } + +} 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..57a2cee974 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,28 +2,61 @@ namespace PHPStan\File; -class FileExcluder +use PHPStan\DependencyInjection\GenerateFactory; +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; + +#[GenerateFactory(interface: FileExcluderRawFactory::class)] +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 +66,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 index 53ba207582..71c3c3b598 100644 --- a/src/File/FileExcluderFactory.php +++ b/src/File/FileExcluderFactory.php @@ -2,39 +2,30 @@ namespace PHPStan\File; -class FileExcluderFactory +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use function array_key_exists; +use function array_merge; +use function array_unique; +use function array_values; + +#[AutowiredService] +final class FileExcluderFactory { - private FileExcluderRawFactory $fileExcluderRawFactory; - - /** @var string[] */ - private array $obsoleteExcludesAnalyse; - - /** @var array{analyse?: array, analyseAndScan?: array}|null */ - private ?array $excludePaths; - /** - * @param FileExcluderRawFactory $fileExcluderRawFactory - * @param string[] $obsoleteExcludesAnalyse - * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + * @param array{analyse?: array, analyseAndScan?: array} $excludePaths */ public function __construct( - FileExcluderRawFactory $fileExcluderRawFactory, - array $obsoleteExcludesAnalyse, - ?array $excludePaths + private FileExcluderRawFactory $fileExcluderRawFactory, + #[AutowiredParameter] + private array $excludePaths, ) { - $this->fileExcluderRawFactory = $fileExcluderRawFactory; - $this->obsoleteExcludesAnalyse = $obsoleteExcludesAnalyse; - $this->excludePaths = $excludePaths; } public function createAnalyseFileExcluder(): FileExcluder { - if ($this->excludePaths === null) { - return $this->fileExcluderRawFactory->create($this->obsoleteExcludesAnalyse); - } - $paths = []; if (array_key_exists('analyse', $this->excludePaths)) { $paths = $this->excludePaths['analyse']; @@ -48,10 +39,6 @@ public function createAnalyseFileExcluder(): FileExcluder public function createScanFileExcluder(): FileExcluder { - if ($this->excludePaths === null) { - return $this->fileExcluderRawFactory->create($this->obsoleteExcludesAnalyse); - } - $paths = []; if (array_key_exists('analyseAndScan', $this->excludePaths)) { $paths = $this->excludePaths['analyseAndScan']; diff --git a/src/File/FileExcluderRawFactory.php b/src/File/FileExcluderRawFactory.php index b2f7e4ee77..0e3550cb3a 100644 --- a/src/File/FileExcluderRawFactory.php +++ b/src/File/FileExcluderRawFactory.php @@ -7,10 +7,9 @@ interface FileExcluderRawFactory /** * @param string[] $analyseExcludes - * @return FileExcluder */ public function create( - array $analyseExcludes + array $analyseExcludes, ): FileExcluder; } diff --git a/src/File/FileFinder.php b/src/File/FileFinder.php index 61469de531..34ab2c5a16 100644 --- a/src/File/FileFinder.php +++ b/src/File/FileFinder.php @@ -3,46 +3,39 @@ namespace PHPStan\File; use Symfony\Component\Finder\Finder; - -class FileFinder +use function array_filter; +use function array_unique; +use function array_values; +use function file_exists; +use function implode; +use function is_file; + +final class FileFinder { - private FileExcluder $fileExcluder; - - private FileHelper $fileHelper; - - /** @var string[] */ - private array $fileExtensions; - /** - * @param FileExcluder $fileExcluder - * @param FileHelper $fileHelper * @param string[] $fileExtensions */ public function __construct( - FileExcluder $fileExcluder, - FileHelper $fileHelper, - array $fileExtensions + private FileExcluder $fileExcluder, + private FileHelper $fileHelper, + private array $fileExtensions, ) { - $this->fileExcluder = $fileExcluder; - $this->fileHelper = $fileHelper; - $this->fileExtensions = $fileExtensions; } /** * @param string[] $paths - * @return FileFinderResult */ public function findFiles(array $paths): FileFinderResult { $onlyFiles = true; $files = []; foreach ($paths as $path) { - if (!file_exists($path)) { - throw new \PHPStan\File\PathNotFoundException($path); - } elseif (is_file($path)) { + if (is_file($path)) { $files[] = $this->fileHelper->normalizePath($path); + } elseif (!file_exists($path)) { + throw new PathNotFoundException($path); } else { $finder = new Finder(); $finder->followLinks(); @@ -53,9 +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 eac8c5accc..8c6b32ec8d 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -3,13 +3,33 @@ namespace PHPStan\File; use Nette\Utils\Strings; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class FileHelper { private string $workingDirectory; - public function __construct(string $workingDirectory) + public function __construct( + #[AutowiredParameter(ref: '%currentWorkingDirectory%')] + string $workingDirectory, + ) { $this->workingDirectory = $this->normalizePath($workingDirectory); } @@ -23,15 +43,14 @@ public function getWorkingDirectory(): string public function absolutizePath(string $path): string { if (DIRECTORY_SEPARATOR === '/') { - if (substr($path, 0, 1) === '/') { - return $path; - } - } else { - if (substr($path, 1, 1) === ':') { + if (str_starts_with($path, '/')) { return $path; } + } elseif (substr($path, 1, 1) === ':') { + return $path; } - if (\Nette\Utils\Strings::startsWith($path, 'phar://')) { + + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } @@ -41,18 +60,31 @@ public function absolutizePath(string $path): string /** @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 = []; @@ -61,12 +93,10 @@ public function normalizePath(string $originalPath, string $directorySeparator = continue; } if ($pathPart === '..') { - /** @var string $removedPart */ $removedPart = array_pop($normalizedPathParts); - if ($scheme === 'phar' && substr($removedPart, -5) === '.phar') { + if ($scheme === 'phar' && $removedPart !== null && str_ends_with($removedPart, '.phar')) { $scheme = null; } - } else { $normalizedPathParts[] = $pathPart; } diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index c25856a140..0da0270215 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -2,52 +2,79 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\ShouldNotHappenException; +use function array_diff; use function array_key_exists; - -class FileMonitor +use function array_keys; +use function array_merge; +use function array_unique; +use function is_dir; +use function is_file; +use function sha1_file; + +#[AutowiredService] +final class FileMonitor { - /** @var FileFinder */ - private $fileFinder; - /** @var array|null */ - private $fileHashes; + private ?array $fileHashes = null; /** @var array|null */ - private $paths; + private ?array $filePaths = null; - public function __construct(FileFinder $fileFinder) + /** + * @param string[] $analysedPaths + * @param string[] $analysedPathsFromConfig + * @param string[] $scanFiles + * @param string[] $scanDirectories + */ + public function __construct( + #[AutowiredParameter(ref: '@fileFinderAnalyse')] + private FileFinder $analyseFileFinder, + #[AutowiredParameter(ref: '@fileFinderScan')] + private FileFinder $scanFileFinder, + #[AutowiredParameter] + private array $analysedPaths, + #[AutowiredParameter] + private array $analysedPathsFromConfig, + #[AutowiredParameter] + private array $scanFiles, + #[AutowiredParameter] + private array $scanDirectories, + ) { - $this->fileFinder = $fileFinder; } /** - * @param array $paths + * @param array $filePaths */ - public function initialize(array $paths): void + public function initialize(array $filePaths): void { - $finderResult = $this->fileFinder->findFiles($paths); + $finderResult = $this->analyseFileFinder->findFiles($this->analysedPaths); $fileHashes = []; - foreach ($finderResult->getFiles() as $filePath) { + foreach (array_unique(array_merge($finderResult->getFiles(), $filePaths, $this->getScannedFiles($finderResult->getFiles()))) as $filePath) { $fileHashes[$filePath] = $this->getFileHash($filePath); } $this->fileHashes = $fileHashes; - $this->paths = $paths; + $this->filePaths = $filePaths; } public function getChanges(): FileMonitorResult { - if ($this->fileHashes === null || $this->paths === null) { - throw new \PHPStan\ShouldNotHappenException(); + if ($this->fileHashes === null || $this->filePaths === null) { + throw new ShouldNotHappenException(); } - $finderResult = $this->fileFinder->findFiles($this->paths); + $finderResult = $this->analyseFileFinder->findFiles($this->analysedPaths); $oldFileHashes = $this->fileHashes; $fileHashes = []; $newFiles = []; $changedFiles = []; $deletedFiles = []; - foreach ($finderResult->getFiles() as $filePath) { + $filePaths = array_unique(array_merge($finderResult->getFiles(), $this->filePaths, $this->getScannedFiles($finderResult->getFiles()))); + foreach ($filePaths as $filePath) { if (!array_key_exists($filePath, $oldFileHashes)) { $newFiles[] = $filePath; $fileHashes[$filePath] = $this->getFileHash($filePath); @@ -75,13 +102,46 @@ public function getChanges(): FileMonitorResult $newFiles, $changedFiles, $deletedFiles, - count($fileHashes) ); } private function getFileHash(string $filePath): string { - return sha1(FileReader::read($filePath)); + $hash = sha1_file($filePath); + + if ($hash === false) { + throw new CouldNotReadFileException($filePath); + } + + return $hash; + } + + /** + * @param string[] $allAnalysedFiles + * @return array + */ + private function getScannedFiles(array $allAnalysedFiles): array + { + $scannedFiles = $this->scanFiles; + $analysedDirectories = []; + foreach (array_merge($this->analysedPaths, $this->analysedPathsFromConfig) as $analysedPath) { + if (is_file($analysedPath)) { + continue; + } + + if (!is_dir($analysedPath)) { + continue; + } + + $analysedDirectories[] = $analysedPath; + } + + $directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories)); + foreach ($this->scanFileFinder->findFiles($directories)->getFiles() as $file) { + $scannedFiles[] = $file; + } + + return array_diff($scannedFiles, $allAnalysedFiles); } } diff --git a/src/File/FileMonitorResult.php b/src/File/FileMonitorResult.php index 5e58357941..f76ae9dde4 100644 --- a/src/File/FileMonitorResult.php +++ b/src/File/FileMonitorResult.php @@ -2,38 +2,30 @@ namespace PHPStan\File; -class FileMonitorResult -{ - - /** @var string[] */ - private $newFiles; - - /** @var string[] */ - private $changedFiles; - - /** @var string[] */ - private $deletedFiles; +use function count; - /** @var int */ - private $totalFilesCount; +final class FileMonitorResult +{ /** * @param string[] $newFiles * @param string[] $changedFiles * @param string[] $deletedFiles - * @param int $totalFilesCount */ public function __construct( - array $newFiles, - array $changedFiles, - array $deletedFiles, - int $totalFilesCount + private array $newFiles, + private array $changedFiles, + private array $deletedFiles, ) { - $this->newFiles = $newFiles; - $this->changedFiles = $changedFiles; - $this->deletedFiles = $deletedFiles; - $this->totalFilesCount = $totalFilesCount; + } + + /** + * @return string[] + */ + public function getChangedFiles(): array + { + return $this->changedFiles; } public function hasAnyChanges(): bool @@ -43,9 +35,4 @@ public function hasAnyChanges(): bool || 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 89deb459fc..5e757df06f 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -2,29 +2,42 @@ namespace PHPStan\File; -class FuzzyRelativePathHelper implements RelativePathHelper +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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; + +#[AutowiredService(name: 'relativePathHelper', as: RelativePathHelper::class)] +final class FuzzyRelativePathHelper implements RelativePathHelper { - private RelativePathHelper $fallbackRelativePathHelper; - private string $directorySeparator; private ?string $pathToTrim = null; /** - * @param RelativePathHelper $fallbackRelativePathHelper - * @param string $currentWorkingDirectory * @param string[] $analysedPaths * @param non-empty-string|null $directorySeparator */ public function __construct( - RelativePathHelper $fallbackRelativePathHelper, + #[AutowiredParameter(ref: '@parentDirectoryRelativePathHelper')] + private RelativePathHelper $fallbackRelativePathHelper, + #[AutowiredParameter] string $currentWorkingDirectory, + #[AutowiredParameter] array $analysedPaths, - ?string $directorySeparator = null + ?string $directorySeparator = null, ) { - $this->fallbackRelativePathHelper = $fallbackRelativePathHelper; if ($directorySeparator === null) { $directorySeparator = DIRECTORY_SEPARATOR; } @@ -33,7 +46,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), @@ -54,17 +67,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])) { @@ -101,7 +113,7 @@ public function getRelativePath(string $filename): string { if ( $this->pathToTrim !== null - && strpos($filename, $this->pathToTrim) === 0 + && str_starts_with($filename, $this->pathToTrim) ) { return ltrim(substr($filename, strlen($this->pathToTrim)), $this->directorySeparator); } diff --git a/src/File/NullRelativePathHelper.php b/src/File/NullRelativePathHelper.php index 1556984a90..5e5a07dc62 100644 --- a/src/File/NullRelativePathHelper.php +++ b/src/File/NullRelativePathHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\File; -class NullRelativePathHelper implements RelativePathHelper +final class NullRelativePathHelper implements RelativePathHelper { public function getRelativePath(string $filename): string diff --git a/src/File/ParentDirectoryRelativePathHelper.php b/src/File/ParentDirectoryRelativePathHelper.php index 54582fd5ff..bea0edb077 100644 --- a/src/File/ParentDirectoryRelativePathHelper.php +++ b/src/File/ParentDirectoryRelativePathHelper.php @@ -2,17 +2,29 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\NonAutowiredService; +use PHPStan\ShouldNotHappenException; +use function array_fill; +use function array_merge; use function array_slice; +use function count; +use function explode; +use function implode; use function str_replace; +use function strpos; +use function substr; +use function trim; -class ParentDirectoryRelativePathHelper implements RelativePathHelper +#[NonAutowiredService(name: 'parentDirectoryRelativePathHelper')] +final class ParentDirectoryRelativePathHelper implements RelativePathHelper { - private string $parentDirectory; - - public function __construct(string $parentDirectory) + public function __construct( + #[AutowiredParameter(ref: '%currentWorkingDirectory%')] + private string $parentDirectory, + ) { - $this->parentDirectory = $parentDirectory; } public function getRelativePath(string $filename): string @@ -21,7 +33,6 @@ public function getRelativePath(string $filename): string } /** - * @param string $filename * @return string[] */ public function getFilenameParts(string $filename): array @@ -56,7 +67,7 @@ public function getFilenameParts(string $filename): array $dotsCount = $parentPartsCount - $i; if ($dotsCount < 0) { - throw new \PHPStan\ShouldNotHappenException(); + 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..9dc613ccb7 100644 --- a/src/File/PathNotFoundException.php +++ b/src/File/PathNotFoundException.php @@ -2,20 +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) { parent::__construct(sprintf('Path %s does not exist', $path)); - $this->path = $path; - } - - public function getPath(): string - { - return $this->path; } } diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index a0633a00fa..22c6221871 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -2,19 +2,27 @@ namespace PHPStan\File; -class SimpleRelativePathHelper implements RelativePathHelper -{ +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\NonAutowiredService; +use function str_replace; +use function str_starts_with; +use function strlen; +use function substr; - private string $currentWorkingDirectory; +#[NonAutowiredService(name: 'simpleRelativePathHelper')] +final class SimpleRelativePathHelper implements RelativePathHelper +{ - public function __construct(string $currentWorkingDirectory) + public function __construct( + #[AutowiredParameter(ref: '%currentWorkingDirectory%')] + private string $currentWorkingDirectory, + ) { - $this->currentWorkingDirectory = $currentWorkingDirectory; } public function getRelativePath(string $filename): string { - if ($this->currentWorkingDirectory !== '' && strpos($filename, $this->currentWorkingDirectory) === 0) { + if ($this->currentWorkingDirectory !== '' && str_starts_with($filename, $this->currentWorkingDirectory)) { return str_replace('\\', '/', substr($filename, strlen($this->currentWorkingDirectory) + 1)); } diff --git a/src/File/SystemAgnosticSimpleRelativePathHelper.php b/src/File/SystemAgnosticSimpleRelativePathHelper.php new file mode 100644 index 0000000000..bd4ac68d67 --- /dev/null +++ b/src/File/SystemAgnosticSimpleRelativePathHelper.php @@ -0,0 +1,26 @@ +fileHelper->getWorkingDirectory(); + if ($cwd !== '' && str_starts_with($filename, $cwd)) { + return substr($filename, strlen($cwd) + 1); + } + + return $filename; + } + +} diff --git a/src/Fixable/FileChangedException.php b/src/Fixable/FileChangedException.php new file mode 100644 index 0000000000..89eaf7ac6f --- /dev/null +++ b/src/Fixable/FileChangedException.php @@ -0,0 +1,10 @@ +differ = new Differ(new UnifiedDiffOutputBuilder()); + } + + /** + * @param FixedErrorDiff[] $diffs + * @throws FileChangedException + * @throws MergeConflictException + */ + public function applyDiffs(string $fileName, array $diffs): string + { + $fileContents = FileReader::read($fileName); + $fileHash = sha1($fileContents); + $diffHunks = []; + foreach ($diffs as $diff) { + if ($diff->originalHash !== $fileHash) { + throw new FileChangedException(); + } + + $diffHunks[] = Hunk::createArray(Line::createArray($this->reconstructFullDiff($fileContents, $diff->diff))); + } + + if (count($diffHunks) === 0) { + return $fileContents; + } + + $baseLines = Line::createArray(array_map( + static fn ($l) => [$l, Differ::OLD], + self::splitStringByLines($fileContents), + )); + + $refMerge = new ReflectionClass(PhpMerge::class); + $refMergeMethod = $refMerge->getMethod('mergeHunks'); + if (PHP_VERSION_ID < 80100) { + $refMergeMethod->setAccessible(true); + } + + $result = Line::createArray(array_map( + static fn ($l) => [$l, Differ::OLD], + $refMergeMethod->invokeArgs(null, [ + $baseLines, + $diffHunks[0], + [], + ]), + )); + + for ($i = 0; $i < count($diffHunks); $i++) { + /** @var MergeConflict[] $conflicts */ + $conflicts = []; + $merged = $refMergeMethod->invokeArgs(null, [ + $baseLines, + Hunk::createArray(Line::createArray($this->differ->diffToArray($fileContents, implode('', array_map(static fn ($l) => $l->getContent(), $result))))), + $diffHunks[$i], + &$conflicts, + ]); + if (count($conflicts) > 0) { + throw new MergeConflictException(); + } + + $result = Line::createArray(array_map( + static fn ($l) => [$l, Differ::OLD], + $merged, + )); + + } + + return implode('', array_map(static fn ($l) => $l->getContent(), $result)); + } + + /** + * @return array + */ + private function reconstructFullDiff(string $originalText, string $unifiedDiff): array + { + $originalLines = self::splitStringByLines($originalText); + $diffLines = self::splitStringByLines($unifiedDiff); + $result = []; + + $origLineNo = 0; + $diffPos = 0; + + while ($diffPos < count($diffLines)) { + $line = $diffLines[$diffPos]; + + $matches = Strings::match($line, '/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/'); + if ($matches !== null) { + // Parse hunk header + $origStart = (int) $matches[1] - 1; // 0-based + $diffPos++; + + // Emit kept lines before hunk + while ($origLineNo < $origStart) { + $result[] = [$originalLines[$origLineNo], Differ::OLD]; + $origLineNo++; + } + + // Process hunk + while ($diffPos < count($diffLines)) { + $line = $diffLines[$diffPos]; + if (str_starts_with($line, '@@')) { + break; // next hunk + } + + $prefix = $line[0] ?? ''; + $content = substr($line, 1); + + if ($prefix === ' ') { + $result[] = [$content, Differ::OLD]; + $origLineNo++; + } elseif ($prefix === '-') { + $result[] = [$content, Differ::REMOVED]; + $origLineNo++; + } elseif ($prefix === '+') { + $result[] = [$content, Differ::ADDED]; + } + + $diffPos++; + } + } else { + $diffPos++; + } + } + + // Emit remaining lines as kept + while ($origLineNo < count($originalLines)) { + $result[] = [$originalLines[$origLineNo], Differ::OLD]; + $origLineNo++; + } + + return $result; + } + + /** + * @return string[] + */ + private static function splitStringByLines(string $input): array + { + return Strings::split($input, '/(.*\R)/', PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } + +} diff --git a/src/Fixable/PhpDoc/CallbackVisitor.php b/src/Fixable/PhpDoc/CallbackVisitor.php new file mode 100644 index 0000000000..47afdc2156 --- /dev/null +++ b/src/Fixable/PhpDoc/CallbackVisitor.php @@ -0,0 +1,32 @@ +callback = $callback; + } + + /** + * @return Node[]|Node|null + */ + #[Override] + public function enterNode(Node $node): array|Node|null + { + $callback = $this->callback; + + return $callback($node); + } + +} diff --git a/src/Fixable/PhpDoc/PhpDocEditor.php b/src/Fixable/PhpDoc/PhpDocEditor.php new file mode 100644 index 0000000000..85962e47cd --- /dev/null +++ b/src/Fixable/PhpDoc/PhpDocEditor.php @@ -0,0 +1,62 @@ +getDocComment(); + if ($doc === null) { + $phpDoc = '/** */'; + } else { + $phpDoc = $doc->getText(); + } + $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + + $cloningTraverser = new NodeTraverser([new CloningVisitor()]); + + /** @var PhpDocNode $newPhpDocNode */ + [$newPhpDocNode] = $cloningTraverser->traverse([$phpDocNode]); + + $traverser = new NodeTraverser([new CallbackVisitor($callback)]); + + /** @var PhpDocNode $newPhpDocNode */ + [$newPhpDocNode] = $traverser->traverse([$newPhpDocNode]); + + if (count($newPhpDocNode->children) === 0) { + $node->setAttribute('comments', []); + return; + } + + $doc = new Doc($this->printer->printFormatPreserving($newPhpDocNode, $phpDocNode, $tokens)); + $node->setDocComment($doc); + } + +} diff --git a/src/Fixable/PhpPrinter.php b/src/Fixable/PhpPrinter.php new file mode 100644 index 0000000000..4407c15d75 --- /dev/null +++ b/src/Fixable/PhpPrinter.php @@ -0,0 +1,37 @@ +getAttribute(self::FUNC_ARGS_TRAILING_COMMA_ATTRIBUTE); + if ($trailingComma === false) { + $result = rtrim($result, ','); + } + + return $result; + } + +} diff --git a/src/Fixable/PhpPrinterIndentationDetectorVisitor.php b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php new file mode 100644 index 0000000000..af696b5b95 --- /dev/null +++ b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php @@ -0,0 +1,84 @@ +stmts) || count($node->stmts) === 0) { + return null; + } + + $firstStmt = $node->stmts[0]; + if (!$firstStmt instanceof Node) { + return null; + } + $text = $this->origTokens->getTokenCode($node->getStartTokenPos(), $firstStmt->getStartTokenPos(), 0); + + $c = preg_match_all('~\n([\\x09\\x20]*)~', $text, $matches, PREG_SET_ORDER); + if (in_array($c, [0, false], true)) { + return null; + } + + $char = ''; + $size = 0; + foreach ($matches as $match) { + $l = strlen($match[1]); + if ($l === 0) { + continue; + } + + $char = $match[1]; + $size = $l; + break; + } + + if ($size > 0) { + $d = preg_match('~^(\\x20+)$~', $char); + if ($d !== false && $d > 0) { + $size = strlen($char); + $char = ' '; + } + + $this->indentCharacter = $char; + $this->indentSize = $size; + + return NodeVisitor::STOP_TRAVERSAL; + } + + return null; + } + +} diff --git a/src/Fixable/ReplacingNodeVisitor.php b/src/Fixable/ReplacingNodeVisitor.php new file mode 100644 index 0000000000..33be2e7fa0 --- /dev/null +++ b/src/Fixable/ReplacingNodeVisitor.php @@ -0,0 +1,47 @@ +getAttribute('origNode'); + if ($origNode !== $this->originalNode) { + return null; + } + + $this->found = true; + + $callable = $this->newNodeCallable; + $newNode = $callable($node); + if ($newNode instanceof VirtualNode) { + throw new ShouldNotHappenException('Cannot print VirtualNode.'); + } + + return $newNode; + } + + public function isFound(): bool + { + return $this->found; + } + +} diff --git a/src/Fixable/UnwrapVirtualNodesVisitor.php b/src/Fixable/UnwrapVirtualNodesVisitor.php new file mode 100644 index 0000000000..76e5371e67 --- /dev/null +++ b/src/Fixable/UnwrapVirtualNodesVisitor.php @@ -0,0 +1,29 @@ +cond instanceof AlwaysRememberedExpr) { + return null; + } + + $node->cond = $node->cond->expr; + + return $node; + } + +} diff --git a/src/Internal/ArrayHelper.php b/src/Internal/ArrayHelper.php new file mode 100644 index 0000000000..2498f61efb --- /dev/null +++ b/src/Internal/ArrayHelper.php @@ -0,0 +1,27 @@ + $path + */ + public static function unsetKeyAtPath(array &$array, array $path): void + { + [$head, $tail] = [$path[0], array_slice($path, 1)]; + + if (count($tail) === 0) { + unset($array[$head]); + + } elseif (isset($array[$head])) { + self::unsetKeyAtPath($array[$head], $tail); + } + } + +} diff --git a/src/Internal/BytesHelper.php b/src/Internal/BytesHelper.php index 73561e0de5..3279501f51 100644 --- a/src/Internal/BytesHelper.php +++ b/src/Internal/BytesHelper.php @@ -2,7 +2,12 @@ namespace PHPStan\Internal; -class BytesHelper +use PHPStan\ShouldNotHappenException; +use function abs; +use function end; +use function round; + +final class BytesHelper { public static function bytes(int $bytes): string @@ -17,7 +22,7 @@ public static function bytes(int $bytes): string } if (!isset($unit)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return round($bytes, 2) . ' ' . $unit; diff --git a/src/Internal/CombinationsHelper.php b/src/Internal/CombinationsHelper.php new file mode 100644 index 0000000000..feeb550400 --- /dev/null +++ b/src/Internal/CombinationsHelper.php @@ -0,0 +1,35 @@ + $arrays + * @return iterable + */ + public static function combinations(array $arrays): iterable + { + // from https://stackoverflow.com/a/70800936/565782 by Arnaud Le Blanc + if ($arrays === []) { + yield []; + return; + } + + $head = array_shift($arrays); + + foreach ($head as $elem) { + foreach (self::combinations($arrays) as $combination) { + $comb = [$elem]; + foreach ($combination as $c) { + $comb[] = $c; + } + yield $comb; + } + } + } + +} diff --git a/src/Internal/ComposerHelper.php b/src/Internal/ComposerHelper.php new file mode 100644 index 0000000000..e1995bc34d --- /dev/null +++ b/src/Internal/ComposerHelper.php @@ -0,0 +1,91 @@ +|null */ + public static function getComposerConfig(string $root): ?array + { + $composerJsonPath = self::getComposerJsonPath($root); + + if (!is_file($composerJsonPath)) { + return null; + } + + try { + $composerJsonContents = FileReader::read($composerJsonPath); + + return Json::decode($composerJsonContents, Json::FORCE_ARRAY); + } catch (CouldNotReadFileException | JsonException) { + return null; + } + } + + private static function getComposerJsonPath(string $root): string + { + $envComposer = getenv('COMPOSER'); + $fileName = is_string($envComposer) ? $envComposer : 'composer.json'; + $fileName = basename(trim($fileName)); + + return $root . '/' . $fileName; + } + + /** + * @param array $composerConfig + */ + public static function getVendorDirFromComposerConfig(string $root, array $composerConfig): string + { + $vendorDirectory = $composerConfig['config']['vendor-dir'] ?? 'vendor'; + + return $root . '/' . trim($vendorDirectory, '/'); + } + + /** + * @param array $composerConfig + */ + public static function getBinDirFromComposerConfig(string $root, array $composerConfig): string + { + $vendorDirectory = $composerConfig['config']['bin-dir'] ?? 'vendor/bin'; + + return $root . '/' . trim($vendorDirectory, '/'); + } + + public static function getPhpStanVersion(): string + { + if (self::$phpstanVersion !== null) { + return self::$phpstanVersion; + } + + $installed = require __DIR__ . '/../../vendor/composer/installed.php'; + $rootPackage = $installed['root'] ?? null; + if ($rootPackage === null) { + return self::$phpstanVersion = self::UNKNOWN_VERSION; + } + + if (preg_match('/[^v\d.]/', $rootPackage['pretty_version']) === 0) { + // Handles tagged versions, see https://github.com/Jean85/pretty-package-versions/blob/2.0.5/src/Version.php#L31 + return self::$phpstanVersion = $rootPackage['pretty_version']; + } + + return self::$phpstanVersion = $rootPackage['pretty_version'] . '@' . substr((string) $rootPackage['reference'], 0, 7); + } + +} diff --git a/src/Internal/ContainerDynamicReturnTypeExtension.php b/src/Internal/ContainerDynamicReturnTypeExtension.php deleted file mode 100644 index 6b99ec8a4d..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->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $argType = $scope->getType($methodCall->getArgs()[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $type = new ObjectType($argType->getValue()); - if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { - $argType = $scope->getType($methodCall->getArgs()[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $type = TypeCombinator::addNull($type); - } - } - - return $type; - } - -} diff --git a/src/Internal/DeprecatedAttributeHelper.php b/src/Internal/DeprecatedAttributeHelper.php new file mode 100644 index 0000000000..8217fa1ef4 --- /dev/null +++ b/src/Internal/DeprecatedAttributeHelper.php @@ -0,0 +1,45 @@ + $attributes + */ + public static function getDeprecatedDescription(array $attributes): ?string + { + $deprecated = ReflectionAttributeHelper::filterAttributesByName($attributes, 'Deprecated'); + foreach ($deprecated as $attr) { + $arguments = $attr->getArguments(); + foreach ($arguments as $i => $arg) { + if (!is_string($arg)) { + continue; + } + + if (is_int($i)) { + if ($i !== 0) { + continue; + } + + return $arg; + } + + if ($i !== 'message') { + continue; + } + + return $arg; + } + } + + return null; + } + +} diff --git a/src/Internal/DirectoryCreator.php b/src/Internal/DirectoryCreator.php new file mode 100644 index 0000000000..3ed7c2ff11 --- /dev/null +++ b/src/Internal/DirectoryCreator.php @@ -0,0 +1,36 @@ +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, - false, - $scope - ); - } - -} diff --git a/src/Internal/SprintfHelper.php b/src/Internal/SprintfHelper.php index 76f4fa19bd..6938c898f1 100644 --- a/src/Internal/SprintfHelper.php +++ b/src/Internal/SprintfHelper.php @@ -2,7 +2,9 @@ namespace PHPStan\Internal; -class SprintfHelper +use function str_replace; + +final class SprintfHelper { public static function escapeFormatString(string $format): string diff --git a/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php b/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php deleted file mode 100644 index 32ff7b2771..0000000000 --- a/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php +++ /dev/null @@ -1,40 +0,0 @@ -getName() === 'getInternal'; - } - - public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): Type - { - if (count($methodCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $getterClosureType = $scope->getType($methodCall->getArgs()[1]->value); - return ParametersAcceptorSelector::selectSingle($getterClosureType->getCallableParametersAcceptors($scope))->getReturnType(); - } - -} diff --git a/src/Node/AnonymousClassNode.php b/src/Node/AnonymousClassNode.php new file mode 100644 index 0000000000..afed122f56 --- /dev/null +++ b/src/Node/AnonymousClassNode.php @@ -0,0 +1,34 @@ +getSubNodeNames() as $subNodeName) { + $subNodes[$subNodeName] = $node->$subNodeName; + } + + return new AnonymousClassNode( + $node->name, + $subNodes, + $node->getAttributes(), + ); + } + + #[Override] + public function isAnonymous(): bool + { + return true; + } + +} diff --git a/src/Node/BooleanAndNode.php b/src/Node/BooleanAndNode.php index ee0f25b3b4..cf88a8b1bb 100644 --- a/src/Node/BooleanAndNode.php +++ b/src/Node/BooleanAndNode.php @@ -2,29 +2,21 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanAndNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BooleanAndNode extends Expr implements VirtualNode { - /** @var BooleanAnd|LogicalAnd */ - private $originalNode; - - private Scope $rightScope; - - /** - * @param BooleanAnd|LogicalAnd $originalNode - * @param Scope $rightScope - */ - public function __construct($originalNode, Scope $rightScope) + public function __construct(private BooleanAnd|LogicalAnd $originalNode, private Scope $rightScope) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->rightScope = $rightScope; } /** @@ -40,6 +32,7 @@ public function getRightScope(): Scope return $this->rightScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_BooleanAndNode'; @@ -48,6 +41,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/BooleanOrNode.php b/src/Node/BooleanOrNode.php index 4a43adac5d..49e1e8edd9 100644 --- a/src/Node/BooleanOrNode.php +++ b/src/Node/BooleanOrNode.php @@ -2,29 +2,21 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalOr; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanOrNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BooleanOrNode extends Expr implements VirtualNode { - /** @var BooleanOr|LogicalOr */ - private $originalNode; - - private Scope $rightScope; - - /** - * @param BooleanOr|LogicalOr $originalNode - * @param Scope $rightScope - */ - public function __construct($originalNode, Scope $rightScope) + public function __construct(private BooleanOr|LogicalOr $originalNode, private Scope $rightScope) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->rightScope = $rightScope; } /** @@ -40,6 +32,7 @@ public function getRightScope(): Scope return $this->rightScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_BooleanOrNode'; @@ -48,6 +41,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php index 9ba48fdf8b..4bbfe497f7 100644 --- a/src/Node/BreaklessWhileLoopNode.php +++ b/src/Node/BreaklessWhileLoopNode.php @@ -2,27 +2,23 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\While_; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -/** @api */ -class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode { - private While_ $originalNode; - - /** @var StatementExitPoint[] */ - private array $exitPoints; - /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(While_ $originalNode, array $exitPoints) + public function __construct(private While_ $originalNode, private array $exitPoints) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->exitPoints = $exitPoints; } public function getOriginalNode(): While_ @@ -38,6 +34,7 @@ public function getExitPoints(): array return $this->exitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_BreaklessWhileLoop'; @@ -46,6 +43,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php index 6c367e32d4..9288d863c1 100644 --- a/src/Node/CatchWithUnthrownExceptionNode.php +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -2,26 +2,20 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\Catch_; use PhpParser\NodeAbstract; use PHPStan\Type\Type; -/** @api */ -class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode { - private Catch_ $originalNode; - - private Type $caughtType; - - private Type $originalCaughtType; - - public function __construct(Catch_ $originalNode, Type $caughtType, Type $originalCaughtType) + public function __construct(private Catch_ $originalNode, private Type $caughtType, private Type $originalCaughtType) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->caughtType = $caughtType; - $this->originalCaughtType = $originalCaughtType; } public function getOriginalNode(): Catch_ @@ -39,6 +33,7 @@ public function getOriginalCaughtType(): Type return $this->originalCaughtType; } + #[Override] public function getType(): string { return 'PHPStan_Node_CatchWithUnthrownExceptionNode'; @@ -47,6 +42,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index 3ccb91441b..fbc120199d 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -2,34 +2,26 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Reflection\ClassReflection; -/** @api */ -class ClassConstantsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassConstantsNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassConst[] */ - private array $constants; - - /** @var ClassConstantFetch[] */ - private array $fetches; - /** - * @param ClassLike $class * @param ClassConst[] $constants * @param ClassConstantFetch[] $fetches */ - public function __construct(ClassLike $class, array $constants, array $fetches) + public function __construct(private ClassLike $class, private array $constants, private array $fetches, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->constants = $constants; - $this->fetches = $fetches; } public function getClass(): ClassLike @@ -53,17 +45,24 @@ public function getFetches(): array return $this->fetches; } + #[Override] public function getType(): string { - return 'PHPStan_Node_ClassPropertiesNode'; + return 'PHPStan_Node_ClassConstantsNode'; } /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassMethod.php b/src/Node/ClassMethod.php new file mode 100644 index 0000000000..2aec877cf5 --- /dev/null +++ b/src/Node/ClassMethod.php @@ -0,0 +1,28 @@ +node; + } + + public function isDeclaredInTrait(): bool + { + return $this->isDeclaredInTrait; + } + +} diff --git a/src/Node/ClassMethodsNode.php b/src/Node/ClassMethodsNode.php index 2f029cea34..bdb30ae3d2 100644 --- a/src/Node/ClassMethodsNode.php +++ b/src/Node/ClassMethodsNode.php @@ -2,34 +2,25 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; use PHPStan\Node\Method\MethodCall; +use PHPStan\Reflection\ClassReflection; -/** @api */ -class ClassMethodsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassMethodsNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassMethod[] */ - private array $methods; - - /** @var array */ - private array $methodCalls; - /** - * @param ClassLike $class * @param ClassMethod[] $methods * @param array $methodCalls */ - public function __construct(ClassLike $class, array $methods, array $methodCalls) + public function __construct(private ClassLike $class, private array $methods, private array $methodCalls, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->methods = $methods; - $this->methodCalls = $methodCalls; } public function getClass(): ClassLike @@ -53,6 +44,7 @@ public function getMethodCalls(): array return $this->methodCalls; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassMethodsNode'; @@ -61,9 +53,15 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 3f0dde0cd2..5f47c928b5 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -2,6 +2,8 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; @@ -10,42 +12,48 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Node\Method\MethodCall; +use PHPStan\Node\Property\PropertyAssign; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\TrinaryLogic; +use PHPStan\Type\NeverType; +use PHPStan\Type\TypeUtils; +use function array_diff_key; +use function array_key_exists; +use function array_keys; +use function in_array; +use function strtolower; -/** @api */ -class ClassPropertiesNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassPropertiesNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassPropertyNode[] */ - private array $properties; - - /** @var array */ - private array $propertyUsages; - - /** @var array */ - private array $methodCalls; - /** - * @param ClassLike $class * @param ClassPropertyNode[] $properties * @param array $propertyUsages * @param array $methodCalls + * @param array $returnStatementNodes + * @param list $propertyAssigns */ - public function __construct(ClassLike $class, array $properties, array $propertyUsages, array $methodCalls) + public function __construct( + private ClassLike $class, + private ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private array $properties, + private array $propertyUsages, + private array $methodCalls, + private array $returnStatementNodes, + private array $propertyAssigns, + private ClassReflection $classReflection, + ) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->properties = $properties; - $this->propertyUsages = $propertyUsages; - $this->methodCalls = $methodCalls; } public function getClass(): ClassLike @@ -69,6 +77,7 @@ public function getPropertyUsages(): array return $this->propertyUsages; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassPropertiesNode'; @@ -77,64 +86,90 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + /** * @param string[] $constructors - * @param ReadWritePropertiesExtension[] $extensions - * @return array{array, array} + * @return array{array, array, array} */ public function getUninitializedProperties( Scope $scope, array $constructors, - array $extensions ): array { if (!$this->getClass() instanceof Class_) { - return [[], []]; - } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + return [[], [], []]; } - $classReflection = $scope->getClassReflection(); + $classReflection = $this->getClassReflection(); - $properties = []; + $uninitializedProperties = []; + $originalProperties = []; + $initialInitializedProperties = []; + $initializedProperties = []; + $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); + $initializedViaExtension = []; foreach ($this->getProperties() as $property) { if ($property->isStatic()) { continue; } + if ($property->isAbstract()) { + continue; + } if ($property->getNativeType() === null) { continue; } if ($property->getDefault() !== null) { continue; } - $properties[$property->getName()] = $property; - } - - foreach (array_keys($properties) as $name) { - foreach ($extensions as $extension) { - if (!$classReflection->hasNativeProperty($name)) { + $originalProperties[$property->getName()] = $property; + $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); + if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { + $propertyReflection = $classReflection->getNativeProperty($property->getName()); + if ($propertyReflection->isVirtual()->yes()) { continue; } - $propertyReflection = $classReflection->getNativeProperty($name); - if (!$extension->isInitialized($propertyReflection, $name)) { - continue; + + foreach ($extensions as $extension) { + if (!$extension->isInitialized($propertyReflection, $property->getName())) { + continue; + } + $is = TrinaryLogic::createYes(); + $initializedViaExtension[$property->getName()] = true; + break; } - unset($properties[$name]); - break; } + $initialInitializedProperties[$property->getName()] = $is; + foreach ($constructors as $constructor) { + $initializedProperties[$constructor][$property->getName()] = $is; + } + if ($is->yes()) { + continue; + } + $uninitializedProperties[$property->getName()] = $property; } if ($constructors === []) { - return [$properties, []]; + return [$uninitializedProperties, [], []]; } - $classType = new ObjectType($scope->getClassReflection()->getName()); - $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classType, $this->methodCalls, $constructors); + + $initializedInConstructor = []; + if ($classReflection->hasConstructor()) { + $initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties)); + } + + $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor); $prematureAccess = []; + $additionalAssigns = []; + foreach ($this->getPropertyUsages() as $usage) { $fetch = $usage->getFetch(); if (!$fetch instanceof PropertyFetch) { @@ -151,55 +186,153 @@ public function getUninitializedProperties( if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } - if (!in_array($function->getName(), $methodsCalledFromConstructor, true)) { + if (!array_key_exists($function->getName(), $methodsCalledFromConstructor)) { continue; } + $initializedPropertiesMap = $methodsCalledFromConstructor[$function->getName()]; + if (!$fetch->name instanceof Identifier) { continue; } $propertyName = $fetch->name->toString(); - if (!array_key_exists($propertyName, $properties)) { + $fetchedOnType = $usageScope->getType($fetch->var); + if (TypeUtils::findThisType($fetchedOnType) === null) { continue; } - $fetchedOnType = $usageScope->getType($fetch->var); - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { + + $propertyReflection = $usageScope->getInstancePropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { continue; } - if ($fetchedOnType instanceof MixedType) { + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } if ($usage instanceof PropertyWrite) { - unset($properties[$propertyName]); - } elseif (array_key_exists($propertyName, $properties)) { - $prematureAccess[] = [ - $propertyName, - $fetch->getLine(), - ]; + 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 [ - $properties, + $this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties), $prematureAccess, + $additionalAssigns, ]; } /** - * @param ObjectType $classType - * @param MethodCall[] $methodCalls + * @param list $constructors + * @param array $uninitializedProperties + * @return array + */ + private function collectUninitializedProperties(array $constructors, array $uninitializedProperties): array + { + foreach ($constructors as $constructor) { + $lowerConstructorName = strtolower($constructor); + if (!array_key_exists($lowerConstructorName, $this->returnStatementNodes)) { + continue; + } + + $returnStatementsNode = $this->returnStatementNodes[$lowerConstructorName]; + $methodScope = null; + foreach ($returnStatementsNode->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($statementResult->isAlwaysTerminating()) { + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + } + if ($methodScope === null) { + $methodScope = $statementResult->getScope(); + continue; + } + + $methodScope = $methodScope->mergeWith($statementResult->getScope()); + } + + foreach ($returnStatementsNode->getReturnStatements() as $returnStatement) { + if ($methodScope === null) { + $methodScope = $returnStatement->getScope(); + continue; + } + $methodScope = $methodScope->mergeWith($returnStatement->getScope()); + } + + if ($methodScope === null) { + continue; + } + + foreach (array_keys($uninitializedProperties) as $propertyName) { + if (!$methodScope->hasExpressionType(new PropertyInitializationExpr($propertyName))->yes()) { + continue; + } + + unset($uninitializedProperties[$propertyName]); + } + } + + return $uninitializedProperties; + } + + /** * @param string[] $methods - * @return string[] + * @param array $initialInitializedProperties + * @param array> $initializedProperties + * @param array $initializedInConstructorProperties + * + * @return array> */ private function getMethodsCalledFromConstructor( - ObjectType $classType, - array $methodCalls, - array $methods + ClassReflection $classReflection, + array $initialInitializedProperties, + array $initializedProperties, + array $methods, + array $initializedInConstructorProperties, ): array { - $originalCount = count($methods); - foreach ($methodCalls as $methodCall) { + $originalMap = $initializedProperties; + $originalMethods = $methods; + + foreach ($this->methodCalls as $methodCall) { $methodCallNode = $methodCall->getNode(); if ($methodCallNode instanceof Array_) { continue; @@ -208,7 +341,7 @@ private function getMethodsCalledFromConstructor( continue; } $callScope = $methodCall->getScope(); - if ($methodCallNode instanceof \PhpParser\Node\Expr\MethodCall) { + if ($methodCallNode instanceof Node\Expr\MethodCall) { $calledOnType = $callScope->getType($methodCallNode->var); } else { if (!$methodCallNode->class instanceof Name) { @@ -217,31 +350,69 @@ private function getMethodsCalledFromConstructor( $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); } - if ($classType->isSuperTypeOf($calledOnType)->no()) { + + if (TypeUtils::findThisType($calledOnType) === null) { continue; } - if ($calledOnType instanceof MixedType) { + + $inMethod = $callScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { continue; } + if (!in_array($inMethod->getName(), $methods, true)) { + continue; + } + + if ($inMethod->getName() !== '__construct') { + foreach (array_keys($initializedInConstructorProperties) as $propertyName) { + $initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes(); + } + } + $methodName = $methodCallNode->name->toString(); - if (in_array($methodName, $methods, true)) { + if (array_key_exists($methodName, $initializedProperties)) { + foreach ($this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties) as $propertyName => $isInitialized) { + $initializedProperties[$methodName][$propertyName] = $initializedProperties[$methodName][$propertyName]->and($isInitialized); + } continue; } - $inMethod = $callScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { continue; } - if (!in_array($inMethod->getName(), $methods, true)) { + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } + $initializedProperties[$methodName] = $this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties); $methods[] = $methodName; } - if ($originalCount === count($methods)) { - return $methods; + if ($originalMap === $initializedProperties && $originalMethods === $methods) { + return $initializedProperties; } - return $this->getMethodsCalledFromConstructor($classType, $methodCalls, $methods); + 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 index b1d7598e3d..89590f1472 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -2,55 +2,44 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Expr; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; -use PhpParser\Node\Stmt\Class_; use PhpParser\NodeAbstract; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Type\Type; -/** @api */ -class ClassPropertyNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassPropertyNode extends NodeAbstract implements VirtualNode { - private string $name; - - private int $flags; - - /** @var Identifier|Name|Node\ComplexType|null */ - private $type; - - private ?Expr $default; - - private ?string $phpDoc; - - private bool $isPromoted; - /** - * @param int $flags - * @param Identifier|Name|Node\ComplexType|null $type - * @param string $name - * @param Expr|null $default + * @param non-empty-string $name */ public function __construct( - string $name, - int $flags, - $type, - ?Expr $default, - ?string $phpDoc, - bool $isPromoted, - Node $originalNode + private string $name, + private int $flags, + private ?Type $type, + private ?Expr $default, + private ?string $phpDoc, + private ?Type $phpDocType, + private bool $isPromoted, + private bool $isPromotedFromTrait, + private Node\Stmt\Property|Node\Param $originalNode, + private bool $isReadonlyByPhpDoc, + private bool $isDeclaredInTrait, + private bool $isReadonlyClass, + private bool $isAllowedPrivateMutation, + private ClassReflection $classReflection, ) { parent::__construct($originalNode->getAttributes()); - $this->name = $name; - $this->flags = $flags; - $this->type = $type; - $this->default = $default; - $this->isPromoted = $isPromoted; - $this->phpDoc = $phpDoc; } + /** @return non-empty-string */ public function getName(): string { return $this->name; @@ -71,45 +60,91 @@ public function isPromoted(): bool return $this->isPromoted; } + public function isPromotedFromTrait(): bool + { + return $this->isPromotedFromTrait; + } + public function getPhpDoc(): ?string { return $this->phpDoc; } + public function getPhpDocType(): ?Type + { + return $this->phpDocType; + } + public function isPublic(): bool { - return ($this->flags & Class_::MODIFIER_PUBLIC) !== 0 - || ($this->flags & Class_::VISIBILITY_MODIFIER_MASK) === 0; + return ($this->flags & Modifiers::PUBLIC) !== 0 + || ($this->flags & Modifiers::VISIBILITY_MASK) === 0; } public function isProtected(): bool { - return (bool) ($this->flags & Class_::MODIFIER_PROTECTED); + return (bool) ($this->flags & Modifiers::PROTECTED); } public function isPrivate(): bool { - return (bool) ($this->flags & Class_::MODIFIER_PRIVATE); + return (bool) ($this->flags & Modifiers::PRIVATE); + } + + public function isFinal(): bool + { + return (bool) ($this->flags & Modifiers::FINAL); } public function isStatic(): bool { - return (bool) ($this->flags & Class_::MODIFIER_STATIC); + return (bool) ($this->flags & Modifiers::STATIC); } public function isReadOnly(): bool { - return (bool) ($this->flags & Class_::MODIFIER_READONLY); + return (bool) ($this->flags & Modifiers::READONLY) || $this->isReadonlyClass; + } + + public function isReadOnlyByPhpDoc(): bool + { + return $this->isReadonlyByPhpDoc; + } + + public function isDeclaredInTrait(): bool + { + return $this->isDeclaredInTrait; + } + + public function isAllowedPrivateMutation(): bool + { + return $this->isAllowedPrivateMutation; + } + + public function isAbstract(): bool + { + return (bool) ($this->flags & Modifiers::ABSTRACT); + } + + public function getNativeType(): ?Type + { + return $this->type; } /** - * @return Identifier|Name|Node\ComplexType|null + * @return Node\Identifier|Node\Name|Node\ComplexType|null */ - public function getNativeType() + public function getNativeTypeNode() { - return $this->type; + return $this->originalNode->type; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + #[Override] public function getType(): string { return 'PHPStan_Node_ClassPropertyNode'; @@ -118,9 +153,38 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; } + public function hasHooks(): bool + { + return $this->getHooks() !== []; + } + + /** + * @return Node\PropertyHook[] + */ + public function getHooks(): array + { + return $this->originalNode->hooks; + } + + public function isVirtual(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isVirtual()->yes(); + } + + public function isWritable(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isWritable(); + } + + public function isReadable(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isReadable(); + } + } diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 14baad972b..a2bb6889d5 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; @@ -12,22 +13,32 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Node\Property\PropertyAssign; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\TypeUtils; +use ReflectionProperty; +use function count; +use function in_array; +use function strtolower; -class ClassStatementsGatherer +final class ClassStatementsGatherer { - private ClassReflection $classReflection; + private const PROPERTY_ENUMERATING_FUNCTIONS = [ + 'get_object_vars', + 'array_walk', + ]; - /** @var callable(\PhpParser\Node $node, Scope $scope): void */ + /** @var callable(Node $node, Scope $scope): void */ private $nodeCallback; /** @var ClassPropertyNode[] */ private array $properties = []; - /** @var \PhpParser\Node\Stmt\ClassMethod[] */ + /** @var ClassMethod[] */ private array $methods = []; /** @var \PHPStan\Node\Method\MethodCall[] */ @@ -36,22 +47,26 @@ class ClassStatementsGatherer /** @var array */ private array $propertyUsages = []; - /** @var \PhpParser\Node\Stmt\ClassConst[] */ + /** @var Node\Stmt\ClassConst[] */ private array $constants = []; /** @var ClassConstantFetch[] */ private array $constantFetches = []; + /** @var array */ + private array $returnStatementNodes = []; + + /** @var list */ + private array $propertyAssigns = []; + /** - * @param ClassReflection $classReflection - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function __construct( - ClassReflection $classReflection, - callable $nodeCallback + private ClassReflection $classReflection, + callable $nodeCallback, ) { - $this->classReflection = $classReflection; $this->nodeCallback = $nodeCallback; } @@ -64,7 +79,7 @@ public function getProperties(): array } /** - * @return \PhpParser\Node\Stmt\ClassMethod[] + * @return ClassMethod[] */ public function getMethods(): array { @@ -88,7 +103,7 @@ public function getPropertyUsages(): array } /** - * @return \PhpParser\Node\Stmt\ClassConst[] + * @return Node\Stmt\ClassConst[] */ public function getConstants(): array { @@ -103,41 +118,77 @@ public function getConstantFetches(): array return $this->constantFetches; } - public function __invoke(\PhpParser\Node $node, Scope $scope): void + /** + * @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(\PhpParser\Node $node, Scope $scope): void + private function gatherNodes(Node $node, Scope $scope): void { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) { return; } - if ($node instanceof ClassPropertyNode && !$scope->isInTrait()) { + if ($node instanceof ClassPropertyNode) { $this->properties[] = $node; if ($node->isPromoted()) { $this->propertyUsages[] = new PropertyWrite( new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())), - $scope + $scope, + true, ); } return; } - if ($node instanceof \PhpParser\Node\Stmt\ClassMethod && !$scope->isInTrait()) { - $this->methods[] = $node; + if ($node instanceof Node\Stmt\ClassMethod) { + $this->methods[] = new ClassMethod($node, $scope->isInTrait()); return; } - if ($node instanceof \PhpParser\Node\Stmt\ClassConst) { + 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) { @@ -148,6 +199,11 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void $this->constantFetches[] = new ClassConstantFetch($node, $scope); return; } + if ($node instanceof PropertyAssignNode) { + $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); + $this->propertyAssigns[] = new PropertyAssign($node, $scope); + return; + } if (!$node instanceof Expr) { return; } @@ -155,10 +211,27 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void $this->gatherNodes($node->var, $scope); return; } - if ($node instanceof \PhpParser\Node\Scalar\EncapsedStringPart) { + if ($node instanceof Expr\AssignRef) { + if (!$node->expr instanceof PropertyFetch && !$node->expr instanceof StaticPropertyFetch) { + $this->gatherNodes($node->expr, $scope); + return; + } + + $this->propertyUsages[] = new PropertyRead($node->expr, $scope); + $this->propertyUsages[] = new PropertyWrite($node->expr, $scope, 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; } @@ -166,10 +239,60 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void return; } - if ($inAssign) { - $this->propertyUsages[] = new PropertyWrite($node, $scope); - } else { - $this->propertyUsages[] = new PropertyRead($node, $scope); + $this->propertyUsages[] = new PropertyRead($node, $scope); + } + + 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 07b0a67710..87b63a60a4 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -2,44 +2,41 @@ namespace PHPStan\Node; +use Override; +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; -/** @api */ -class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - private \PhpParser\Node\Expr\Closure $closureExpr; - - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - /** @var array */ - private array $yieldStatements; - - private StatementResult $statementResult; + private Node\Expr\Closure $closureExpr; /** - * @param \PhpParser\Node\Expr\Closure $closureExpr - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param array $yieldStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( Closure $closureExpr, - array $returnStatements, - array $yieldStatements, - 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->yieldStatements = $yieldStatements; - $this->statementResult = $statementResult; } public function getClosureExpr(): Closure @@ -47,22 +44,36 @@ 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; } - /** - * @return array - */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getYieldStatements(): array { return $this->yieldStatements; } + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getStatementResult(): StatementResult { return $this->statementResult; @@ -73,6 +84,7 @@ public function returnsByRef(): bool return $this->closureExpr->byRef; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClosureReturnStatementsNode'; @@ -81,6 +93,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/CollectedDataNode.php b/src/Node/CollectedDataNode.php new file mode 100644 index 0000000000..14a12815fe --- /dev/null +++ b/src/Node/CollectedDataNode.php @@ -0,0 +1,73 @@ + + * @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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_CollectedDataNode'; + } + + /** + * @return array{} + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Constant/ClassConstantFetch.php b/src/Node/Constant/ClassConstantFetch.php index 0e51150823..bda533900b 100644 --- a/src/Node/Constant/ClassConstantFetch.php +++ b/src/Node/Constant/ClassConstantFetch.php @@ -5,18 +5,14 @@ use PhpParser\Node\Expr\ClassConstFetch; use PHPStan\Analyser\Scope; -/** @api */ -class ClassConstantFetch +/** + * @api + */ +final class ClassConstantFetch { - private ClassConstFetch $node; - - private Scope $scope; - - public function __construct(ClassConstFetch $node, Scope $scope) + public function __construct(private ClassConstFetch $node, private Scope $scope) { - $this->node = $node; - $this->scope = $scope; } public function getNode(): ClassConstFetch diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index 6fdc87cfb0..5e589416eb 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -2,26 +2,20 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode +final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode { - private Expr $cond; - - /** @var StatementExitPoint[] */ - private array $exitPoints; - /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(Expr $cond, array $exitPoints) + public function __construct(private Expr $cond, private array $exitPoints) { parent::__construct($cond->getAttributes()); - $this->cond = $cond; - $this->exitPoints = $exitPoints; } public function getCond(): Expr @@ -37,6 +31,7 @@ public function getExitPoints(): array return $this->exitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClosureReturnStatementsNode'; @@ -45,6 +40,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/ExecutionEndNode.php b/src/Node/ExecutionEndNode.php index 48e6bc8441..4eb9699850 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -2,33 +2,27 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementResult; -/** @api */ -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; } @@ -43,6 +37,7 @@ public function hasNativeReturnTypehint(): bool return $this->hasNativeReturnTypehint; } + #[Override] public function getType(): string { return 'PHPStan_Node_ExecutionEndNode'; @@ -51,6 +46,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php new file mode 100644 index 0000000000..71f40b9724 --- /dev/null +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -0,0 +1,48 @@ +expr; + } + + public function getExprType(): Type + { + return $this->type; + } + + public function getNativeExprType(): Type + { + return $this->nativeType; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_AlwaysRememberedExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..95c5aabd5c --- /dev/null +++ b/src/Node/Expr/ExistingArrayDimFetch.php @@ -0,0 +1,42 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_ExistingArrayDimFetch'; + } + + /** + * @return string[] + */ + #[Override] + 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..6073173053 --- /dev/null +++ b/src/Node/Expr/GetIterableKeyTypeExpr.php @@ -0,0 +1,37 @@ +expr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_GetIterableKeyTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..642eb4870e --- /dev/null +++ b/src/Node/Expr/GetIterableValueTypeExpr.php @@ -0,0 +1,37 @@ +expr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_GetIterableValueTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..3822685555 --- /dev/null +++ b/src/Node/Expr/GetOffsetValueTypeExpr.php @@ -0,0 +1,42 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_GetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/NativeTypeExpr.php b/src/Node/Expr/NativeTypeExpr.php new file mode 100644 index 0000000000..b3160ed79d --- /dev/null +++ b/src/Node/Expr/NativeTypeExpr.php @@ -0,0 +1,47 @@ +phpdocType; + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_NativeTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..d662dbe488 --- /dev/null +++ b/src/Node/Expr/OriginalPropertyTypeExpr.php @@ -0,0 +1,37 @@ +propertyFetch; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_OriginalPropertyTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..fa1315e9b8 --- /dev/null +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -0,0 +1,37 @@ +variableName; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_ParameterVariableOriginalValueExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..539d928f4a --- /dev/null +++ b/src/Node/Expr/PropertyInitializationExpr.php @@ -0,0 +1,37 @@ +propertyName; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_PropertyInitializationExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..a152f9f329 --- /dev/null +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -0,0 +1,47 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_SetExistingOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..3317f202de --- /dev/null +++ b/src/Node/Expr/SetOffsetValueTypeExpr.php @@ -0,0 +1,47 @@ +var; + } + + public function getDim(): ?Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_SetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..cc822aed90 --- /dev/null +++ b/src/Node/Expr/TypeExpr.php @@ -0,0 +1,42 @@ +exprType; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_TypeExpr'; + } + + /** + * @return string[] + */ + #[Override] + 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..3fe9c7d09e --- /dev/null +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -0,0 +1,42 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_UnsetOffsetExpr'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FileNode.php b/src/Node/FileNode.php index 298cecab67..f8529cd6e0 100644 --- a/src/Node/FileNode.php +++ b/src/Node/FileNode.php @@ -2,33 +2,34 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node; use PhpParser\NodeAbstract; -/** @api */ -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 { return $this->nodes; } + #[Override] public function getType(): string { return 'PHPStan_Node_FileNode'; @@ -37,6 +38,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/FinallyExitPointsNode.php b/src/Node/FinallyExitPointsNode.php index 7cdafb1487..c6636b5a57 100644 --- a/src/Node/FinallyExitPointsNode.php +++ b/src/Node/FinallyExitPointsNode.php @@ -2,28 +2,23 @@ namespace PHPStan\Node; +use Override; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -/** @api */ -class FinallyExitPointsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class FinallyExitPointsNode extends NodeAbstract implements VirtualNode { - /** @var StatementExitPoint[] */ - private array $finallyExitPoints; - - /** @var StatementExitPoint[] */ - private array $tryCatchExitPoints; - /** * @param StatementExitPoint[] $finallyExitPoints * @param StatementExitPoint[] $tryCatchExitPoints */ - public function __construct(array $finallyExitPoints, array $tryCatchExitPoints) + public function __construct(private array $finallyExitPoints, private array $tryCatchExitPoints) { parent::__construct([]); - $this->finallyExitPoints = $finallyExitPoints; - $this->tryCatchExitPoints = $tryCatchExitPoints; } /** @@ -42,6 +37,7 @@ public function getTryCatchExitPoints(): array return $this->tryCatchExitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_FinallyExitPointsNode'; @@ -50,6 +46,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/FunctionCallableNode.php b/src/Node/FunctionCallableNode.php new file mode 100644 index 0000000000..cf7c1ba9fd --- /dev/null +++ b/src/Node/FunctionCallableNode.php @@ -0,0 +1,48 @@ +originalNode->getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\FuncCall + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_FunctionCallableNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index e4db4d265d..cbacab6749 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,47 +2,42 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use function count; -/** @api */ -class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - private Function_ $function; - - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; - - /** @var ExecutionEndNode[] */ - private array $executionEnds; - /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( - Function_ $function, - array $returnStatements, - StatementResult $statementResult, - array $executionEnds + 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->function = $function; - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; - $this->executionEnds = $executionEnds; } - /** - * @return \PHPStan\Node\ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -53,14 +48,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->function->byRef; @@ -71,6 +68,17 @@ 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; + } + + #[Override] public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; @@ -79,9 +87,23 @@ public function getType(): string /** * @return string[] */ + #[Override] 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 a5070b522d..6876978cec 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -2,26 +2,37 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -/** @api */ -class InArrowFunctionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InArrowFunctionNode extends NodeAbstract implements VirtualNode { - private \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; } + #[Override] public function getType(): string { return 'PHPStan_Node_InArrowFunctionNode'; @@ -30,6 +41,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InClassMethodNode.php b/src/Node/InClassMethodNode.php index 8562b138c5..6547b7a12a 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -2,23 +2,42 @@ namespace PHPStan\Node; -/** @api */ -class InClassMethodNode extends \PhpParser\Node\Stmt implements VirtualNode +use Override; +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; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InClassMethodNode'; @@ -27,6 +46,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InClassNode.php b/src/Node/InClassNode.php index cb60bf9086..69c046be1e 100644 --- a/src/Node/InClassNode.php +++ b/src/Node/InClassNode.php @@ -2,22 +2,20 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; use PHPStan\Reflection\ClassReflection; -/** @api */ -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 @@ -30,6 +28,7 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InClassNode'; @@ -38,6 +37,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InClosureNode.php b/src/Node/InClosureNode.php index 4860c1cc55..15ee9acd59 100644 --- a/src/Node/InClosureNode.php +++ b/src/Node/InClosureNode.php @@ -2,26 +2,37 @@ namespace PHPStan\Node; +use Override; +use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -/** @api */ -class InClosureNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InClosureNode extends NodeAbstract implements VirtualNode { - private \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; } + #[Override] public function getType(): string { return 'PHPStan_Node_InClosureNode'; @@ -30,6 +41,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InForeachNode.php b/src/Node/InForeachNode.php new file mode 100644 index 0000000000..be8f02f1cc --- /dev/null +++ b/src/Node/InForeachNode.php @@ -0,0 +1,37 @@ +getAttributes()); + } + + public function getOriginalNode(): Foreach_ + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_InForeachNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index f6457a8a71..6393ef0cdd 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -2,23 +2,35 @@ namespace PHPStan\Node; -/** @api */ -class InFunctionNode extends \PhpParser\Node\Stmt implements VirtualNode +use Override; +use PhpParser\Node; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; + +/** + * @api + */ +final class InFunctionNode extends Node\Stmt implements VirtualNode { - private \PhpParser\Node\Stmt\Function_ $originalNode; - - public function __construct(\PhpParser\Node\Stmt\Function_ $originalNode) + public function __construct( + private 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; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InFunctionNode'; @@ -27,6 +39,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InPropertyHookNode.php b/src/Node/InPropertyHookNode.php new file mode 100644 index 0000000000..4d64857064 --- /dev/null +++ b/src/Node/InPropertyHookNode.php @@ -0,0 +1,63 @@ +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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_InPropertyHookNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InTraitNode.php b/src/Node/InTraitNode.php new file mode 100644 index 0000000000..bc4f677b9e --- /dev/null +++ b/src/Node/InTraitNode.php @@ -0,0 +1,50 @@ +getAttributes()); + } + + public function getOriginalNode(): Node\Stmt\Trait_ + { + return $this->originalNode; + } + + public function getTraitReflection(): ClassReflection + { + return $this->traitReflection; + } + + public function getImplementingClassReflection(): ClassReflection + { + return $this->implementingClassReflection; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Stmt_InTraitNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InstantiationCallableNode.php b/src/Node/InstantiationCallableNode.php new file mode 100644 index 0000000000..e7a9f33f6a --- /dev/null +++ b/src/Node/InstantiationCallableNode.php @@ -0,0 +1,48 @@ +originalNode->getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getClass() + { + return $this->class; + } + + public function getOriginalNode(): Expr\New_ + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_InstantiationCallableNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php new file mode 100644 index 0000000000..72af3a2f71 --- /dev/null +++ b/src/Node/InvalidateExprNode.php @@ -0,0 +1,40 @@ +getAttributes()); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_InvalidateExpr'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/IssetExpr.php b/src/Node/IssetExpr.php new file mode 100644 index 0000000000..9aa4ed2c8b --- /dev/null +++ b/src/Node/IssetExpr.php @@ -0,0 +1,44 @@ +expr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_IssetExpr'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/LiteralArrayItem.php b/src/Node/LiteralArrayItem.php index 3321ca4977..4d9699121b 100644 --- a/src/Node/LiteralArrayItem.php +++ b/src/Node/LiteralArrayItem.php @@ -2,21 +2,17 @@ namespace PHPStan\Node; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; -/** @api */ -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 diff --git a/src/Node/LiteralArrayNode.php b/src/Node/LiteralArrayNode.php index a542e55c4b..3ad49d89d0 100644 --- a/src/Node/LiteralArrayNode.php +++ b/src/Node/LiteralArrayNode.php @@ -2,24 +2,22 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr\Array_; use PhpParser\NodeAbstract; -/** @api */ -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; } /** @@ -30,6 +28,7 @@ public function getItemNodes(): array return $this->itemNodes; } + #[Override] public function getType(): string { return 'PHPStan_Node_LiteralArray'; @@ -38,6 +37,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/MatchExpressionArm.php b/src/Node/MatchExpressionArm.php index bce5ea1f9c..bad6265698 100644 --- a/src/Node/MatchExpressionArm.php +++ b/src/Node/MatchExpressionArm.php @@ -2,23 +2,22 @@ namespace PHPStan\Node; -/** @api */ -class MatchExpressionArm +/** + * @api + */ +final class MatchExpressionArm { - /** @var MatchExpressionArmCondition[] */ - private array $conditions; - - private int $line; - /** * @param MatchExpressionArmCondition[] $conditions - * @param int $line */ - public function __construct(array $conditions, int $line) + public function __construct(private MatchExpressionArmBody $body, private array $conditions, private int $line) + { + } + + public function getBody(): MatchExpressionArmBody { - $this->conditions = $conditions; - $this->line = $line; + return $this->body; } /** diff --git a/src/Node/MatchExpressionArmBody.php b/src/Node/MatchExpressionArmBody.php new file mode 100644 index 0000000000..dbb6f3f917 --- /dev/null +++ b/src/Node/MatchExpressionArmBody.php @@ -0,0 +1,28 @@ +scope; + } + + public function getBody(): Expr + { + return $this->body; + } + +} diff --git a/src/Node/MatchExpressionArmCondition.php b/src/Node/MatchExpressionArmCondition.php index a5f9ec421b..95a291cce3 100644 --- a/src/Node/MatchExpressionArmCondition.php +++ b/src/Node/MatchExpressionArmCondition.php @@ -5,21 +5,14 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionArmCondition +/** + * @api + */ +final class MatchExpressionArmCondition { - private Expr $condition; - - private Scope $scope; - - private int $line; - - public function __construct(Expr $condition, Scope $scope, int $line) + public function __construct(private Expr $condition, private Scope $scope, private int $line) { - $this->condition = $condition; - $this->scope = $scope; - $this->line = $line; } public function getCondition(): Expr diff --git a/src/Node/MatchExpressionNode.php b/src/Node/MatchExpressionNode.php index 5a2dabdc42..7cfd531e3e 100644 --- a/src/Node/MatchExpressionNode.php +++ b/src/Node/MatchExpressionNode.php @@ -2,36 +2,28 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class MatchExpressionNode extends NodeAbstract implements VirtualNode { - private Expr $condition; - - /** @var MatchExpressionArm[] */ - private array $arms; - - private Scope $endScope; - /** - * @param Expr $condition * @param MatchExpressionArm[] $arms */ public function __construct( - Expr $condition, - array $arms, + private Expr $condition, + private array $arms, Expr\Match_ $originalNode, - Scope $endScope + private Scope $endScope, ) { parent::__construct($originalNode->getAttributes()); - $this->condition = $condition; - $this->arms = $arms; - $this->endScope = $endScope; } public function getCondition(): Expr @@ -52,6 +44,7 @@ public function getEndScope(): Scope return $this->endScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_MatchExpression'; @@ -60,6 +53,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Method/MethodCall.php b/src/Node/Method/MethodCall.php index 2138cb2c9c..d3726915a9 100644 --- a/src/Node/Method/MethodCall.php +++ b/src/Node/Method/MethodCall.php @@ -2,31 +2,26 @@ namespace PHPStan\Node\Method; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; -/** @api */ -class MethodCall +/** + * @api + */ +final class MethodCall { - /** @var \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ */ - private $node; - - private Scope $scope; - - /** - * @param \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ $node - * @param Scope $scope - */ - public function __construct($node, Scope $scope) + public function __construct( + private Node\Expr\MethodCall|StaticCall|Array_ $node, + private Scope $scope, + ) { - $this->node = $node; - $this->scope = $scope; } /** - * @return \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ + * @return Node\Expr\MethodCall|StaticCall|Array_ */ public function getNode() { diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php new file mode 100644 index 0000000000..41aef7a962 --- /dev/null +++ b/src/Node/MethodCallableNode.php @@ -0,0 +1,57 @@ +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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_MethodCallableNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index da1cb08c19..c6b37d3c84 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,47 +2,47 @@ namespace PHPStan\Node; +use Override; +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; - -/** @api */ -class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use function count; + +/** + * @api + */ +final class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { private ClassMethod $classMethod; - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; - - /** @var ExecutionEndNode[] */ - private array $executionEnds; - /** - * @param \PhpParser\Node\Stmt\ClassMethod $method - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( ClassMethod $method, - array $returnStatements, - StatementResult $statementResult, - array $executionEnds + private array $returnStatements, + private array $yieldStatements, + private StatementResult $statementResult, + private array $executionEnds, + private array $impurePoints, + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $methodReflection, ) { parent::__construct($method->getAttributes()); $this->classMethod = $method; - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; - $this->executionEnds = $executionEnds; } - /** - * @return \PHPStan\Node\ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -53,14 +53,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->classMethod->byRef; @@ -71,14 +73,54 @@ 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; + } + + #[Override] public function getType(): string { - return 'PHPStan_Node_FunctionReturnStatementsNode'; + return 'PHPStan_Node_MethodReturnStatementsNode'; } /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/NoopExpressionNode.php b/src/Node/NoopExpressionNode.php new file mode 100644 index 0000000000..542cf2073e --- /dev/null +++ b/src/Node/NoopExpressionNode.php @@ -0,0 +1,42 @@ +originalExpr->getAttributes()); + } + + public function getOriginalExpr(): Expr + { + return $this->originalExpr; + } + + public function hasAssign(): bool + { + return $this->hasAssign; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_NoopExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + 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..7478addf8c --- /dev/null +++ b/src/Node/Printer/ExprPrinter.php @@ -0,0 +1,31 @@ +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..a9fb822528 --- /dev/null +++ b/src/Node/Printer/Printer.php @@ -0,0 +1,101 @@ +getExprType()->describe(VerbosityLevel::precise())); + } + + protected function pPHPStan_Node_NativeTypeExpr(NativeTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanNativeType(%s, %s)', $expr->getPhpDocType()->describe(VerbosityLevel::precise()), $expr->getNativeType()->describe(VerbosityLevel::precise())); + } + + protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore + { + return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); + } + + protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); + } + + protected function pPHPStan_Node_SetExistingOffsetValueTypeExpr(SetExistingOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetExistingOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()), $this->p($expr->getValue())); + } + + protected function pPHPStan_Node_AlwaysRememberedExpr(AlwaysRememberedExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanRembered(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_PropertyInitializationExpr(PropertyInitializationExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanPropertyInitialization(%s)', $expr->getPropertyName()); + } + + protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVariableOriginalValueExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName()); + } + + protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr())); + } + +} diff --git a/src/Node/Property/PropertyAssign.php b/src/Node/Property/PropertyAssign.php new file mode 100644 index 0000000000..a88d384a73 --- /dev/null +++ b/src/Node/Property/PropertyAssign.php @@ -0,0 +1,31 @@ +assign; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/Property/PropertyRead.php b/src/Node/Property/PropertyRead.php index 713b574e74..86c220b77f 100644 --- a/src/Node/Property/PropertyRead.php +++ b/src/Node/Property/PropertyRead.php @@ -6,25 +6,17 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyRead +/** + * @api + */ +final class PropertyRead { - /** @var PropertyFetch|StaticPropertyFetch */ - private $fetch; - - private Scope $scope; - - /** - * PropertyWrite constructor. - * - * @param PropertyFetch|StaticPropertyFetch $fetch - * @param Scope $scope - */ - public function __construct($fetch, Scope $scope) + public function __construct( + private PropertyFetch|StaticPropertyFetch $fetch, + private Scope $scope, + ) { - $this->fetch = $fetch; - $this->scope = $scope; } /** diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php index 5d468baa8a..df39b83d0b 100644 --- a/src/Node/Property/PropertyWrite.php +++ b/src/Node/Property/PropertyWrite.php @@ -6,25 +6,14 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyWrite +/** + * @api + */ +final class PropertyWrite { - /** @var PropertyFetch|StaticPropertyFetch */ - private $fetch; - - private Scope $scope; - - /** - * PropertyWrite constructor. - * - * @param PropertyFetch|StaticPropertyFetch $fetch - * @param Scope $scope - */ - public function __construct($fetch, Scope $scope) + public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite) { - $this->fetch = $fetch; - $this->scope = $scope; } /** @@ -40,4 +29,9 @@ public function getScope(): Scope return $this->scope; } + public function isPromotedPropertyWrite(): bool + { + return $this->promotedPropertyWrite; + } + } diff --git a/src/Node/PropertyAssignNode.php b/src/Node/PropertyAssignNode.php new file mode 100644 index 0000000000..7f5b86f03d --- /dev/null +++ b/src/Node/PropertyAssignNode.php @@ -0,0 +1,51 @@ +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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_PropertyAssignNodeNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/PropertyHookReturnStatementsNode.php b/src/Node/PropertyHookReturnStatementsNode.php new file mode 100644 index 0000000000..135acbaac5 --- /dev/null +++ b/src/Node/PropertyHookReturnStatementsNode.php @@ -0,0 +1,114 @@ + $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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_PropertyHookReturnStatementsNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/PropertyHookStatementNode.php b/src/Node/PropertyHookStatementNode.php new file mode 100644 index 0000000000..09b9de5154 --- /dev/null +++ b/src/Node/PropertyHookStatementNode.php @@ -0,0 +1,49 @@ +getAttributes()); + } + + /** + * @return null + */ + public function getReturnType() + { + return null; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_PropertyHookStatementNode'; + } + + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ReturnStatement.php b/src/Node/ReturnStatement.php index 518efc6165..153faf1534 100644 --- a/src/Node/ReturnStatement.php +++ b/src/Node/ReturnStatement.php @@ -2,20 +2,20 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -/** @api */ -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 index c49ebee640..34c28ef538 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -2,6 +2,9 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; /** @api */ @@ -9,12 +12,31 @@ interface ReturnStatementsNode extends VirtualNode { /** - * @return \PHPStan\Node\ReturnStatement[] + * @return list */ public function getReturnStatements(): array; public function getStatementResult(): StatementResult; + /** + * @return list + */ + public function getExecutionEnds(): array; + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array; + public function returnsByRef(): bool; + public function hasNativeReturnTypehint(): bool; + + /** + * @return list + */ + public function getYieldStatements(): array; + + public function isGenerator(): bool; + } diff --git a/src/Node/StaticMethodCallableNode.php b/src/Node/StaticMethodCallableNode.php new file mode 100644 index 0000000000..ece87a2719 --- /dev/null +++ b/src/Node/StaticMethodCallableNode.php @@ -0,0 +1,61 @@ +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; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_StaticMethodCallableNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index 90e8258606..60991bb115 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -2,18 +2,19 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt; -/** @api */ -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 @@ -21,6 +22,7 @@ public function getOriginalStatement(): Stmt return $this->originalStatement; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_UnreachableStatementNode'; @@ -29,9 +31,18 @@ public function getType(): string /** * @return string[] */ + #[Override] 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..cfd3e9c742 --- /dev/null +++ b/src/Node/VarTagChangedExpressionTypeNode.php @@ -0,0 +1,43 @@ +getAttributes()); + } + + public function getVarTag(): VarTag + { + return $this->varTag; + } + + public function getExpr(): Expr + { + return $this->expr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_VarTagChangedExpressionType'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php new file mode 100644 index 0000000000..678d5bb348 --- /dev/null +++ b/src/Node/VariableAssignNode.php @@ -0,0 +1,45 @@ +getAttributes()); + } + + public function getVariable(): Expr\Variable + { + return $this->variable; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_VariableAssignNodeNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/NodeVisitor/StatementOrderVisitor.php b/src/NodeVisitor/StatementOrderVisitor.php deleted file mode 100644 index 867db98fca..0000000000 --- a/src/NodeVisitor/StatementOrderVisitor.php +++ /dev/null @@ -1,89 +0,0 @@ -orderStack = [0]; - $this->depth = 0; - - return null; - } - - /** - * @param Node $node - * @return null - */ - public function enterNode(Node $node) - { - $order = $this->orderStack[count($this->orderStack) - 1]; - $node->setAttribute('statementOrder', $order); - $node->setAttribute('statementDepth', $this->depth); - - if ( - ($node instanceof Node\Expr || $node instanceof Node\Arg) - && count($this->expressionOrderStack) > 0 - ) { - $expressionOrder = $this->expressionOrderStack[count($this->expressionOrderStack) - 1]; - $node->setAttribute('expressionOrder', $expressionOrder); - $node->setAttribute('expressionDepth', $this->expressionDepth); - $this->expressionOrderStack[count($this->expressionOrderStack) - 1] = $expressionOrder + 1; - $this->expressionOrderStack[] = 0; - $this->expressionDepth++; - } - - if (!$node instanceof Node\Stmt) { - return null; - } - - $this->orderStack[count($this->orderStack) - 1] = $order + 1; - $this->orderStack[] = 0; - $this->depth++; - - $this->expressionOrderStack = [0]; - $this->expressionDepth = 0; - - return null; - } - - /** - * @param Node $node - * @return null - */ - public function leaveNode(Node $node) - { - if ($node instanceof Node\Expr) { - array_pop($this->expressionOrderStack); - $this->expressionDepth--; - } - if (!$node instanceof Node\Stmt) { - return null; - } - - array_pop($this->orderStack); - $this->depth--; - - return null; - } - -} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 1099bb341a..8503d826e0 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -2,73 +2,135 @@ 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\Dependency\ExportedNode; +use PHPStan\Analyser\InternalError; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Process\ProcessHelper; -use React\EventLoop\StreamSelectLoop; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; +use React\Socket\TcpServer; use Symfony\Component\Console\Input\InputInterface; +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 escapeshellarg; +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 +#[AutowiredService] +final class ParallelAnalyser { private const DEFAULT_TIMEOUT = 600.0; - private int $internalErrorsCountLimit; - private float $processTimeout; private ProcessPool $processPool; - private int $decoderBufferSize; - public function __construct( - int $internalErrorsCountLimit, + #[AutowiredParameter] + private int $internalErrorsCountLimit, + #[AutowiredParameter(ref: '%parallel.processTimeout%')] float $processTimeout, - int $decoderBufferSize + #[AutowiredParameter(ref: '%parallel.buffer%')] + private int $decoderBufferSize, ) { - $this->internalErrorsCountLimit = $internalErrorsCountLimit; $this->processTimeout = max($processTimeout, self::DEFAULT_TIMEOUT); - $this->decoderBufferSize = $decoderBufferSize; } /** - * @param Schedule $schedule - * @param string $mainScript - * @param \Closure(int): void|null $postFileCallback - * @param string|null $projectConfigFile - * @param string|null $tmpFile - * @param string|null $insteadOfFile - * @return AnalyserResult + * @param Closure(int ): void|null $postFileCallback + * @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, ?string $tmpFile, ?string $insteadOfFile, - 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 = []; + $usedTraitDependencies = []; + $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, &$usedTraitDependencies, &$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, + $internalErrorsCount === 0 ? $usedTraitDependencies : 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, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, $this->decoderBufferSize); - $encoder = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: $this->decoderBufferSize); + $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $decoder->on('data', function (array $data) use (&$jobs, $decoder, $encoder): void { if ($data['action'] !== 'hello') { return; @@ -89,22 +151,22 @@ public function analyse( /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url($serverAddress, PHP_URL_PORT); - $internalErrorsCount = 0; - - $reachedInternalErrorsCountLimit = false; - - $handleError = function (\Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { - $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); + $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 = []; - $exportedNodes = []; for ($i = 0; $i < $numberOfProcesses; $i++) { if (count($jobs) === 0) { break; @@ -130,16 +192,48 @@ public function analyse( 'worker', $projectConfigFile, $commandOptions, - $input + $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$dependencies, &$exportedNodes, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier): void { + $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler): void { + $fileErrors = []; foreach ($json['errors'] as $jsonError) { - if (is_string($jsonError)) { - $internalErrors[] = sprintf('Internal error: %s', $jsonError); - continue; - } + $fileErrors[] = Error::decode($jsonError); + } + foreach ($json['internalErrors'] as $internalJsonError) { + $internalErrors[] = InternalError::decode($internalJsonError); + } + + foreach ($json['filteredPhpErrors'] as $filteredPhpError) { + $filteredPhpErrors[] = Error::decode($filteredPhpError); + } + + foreach ($json['allPhpErrors'] as $allPhpError) { + $allPhpErrors[] = Error::decode($allPhpError); + } + + $locallyIgnoredFileErrors = []; + foreach ($json['locallyIgnoredErrors'] as $locallyIgnoredJsonError) { + $locallyIgnoredFileErrors[] = Error::decode($locallyIgnoredJsonError); + } + + if ($onFileAnalysisHandler !== null) { + $onFileAnalysisHandler($fileErrors, $locallyIgnoredFileErrors, $json['files']); + } + + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + + foreach ($locallyIgnoredFileErrors as $locallyIgnoredFileError) { + $locallyIgnoredErrors[] = $locallyIgnoredFileError; + } - $errors[] = Error::decode($jsonError); + foreach ($json['collectedData'] as $file => $jsonDataByCollector) { + foreach ($jsonDataByCollector as $collectorType => $listOfCollectedData) { + foreach ($listOfCollectedData as $rawCollectedData) { + $collectedData[$file][$collectorType][] = $rawCollectedData; + } + } } /** @@ -150,6 +244,28 @@ public function analyse( $dependencies[$file] = $fileDependencies; } + /** + * @var string $file + * @var array $fileUsedTraitDependencies + */ + foreach ($json['usedTraitDependencies'] as $file => $fileUsedTraitDependencies) { + $usedTraitDependencies[$file] = $fileUsedTraitDependencies; + } + + 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 @@ -158,7 +274,7 @@ public function analyse( if (count($fileExportedNodes) === 0) { continue; } - $exportedNodes[$file] = array_map(static function (array $node): ExportedNode { + $exportedNodes[$file] = array_map(static function (array $node): RootExportedNode { $class = $node['type']; return $class::decode($node['data']); @@ -166,7 +282,11 @@ public function analyse( } if ($postFileCallback !== null) { - $postFileCallback($json['filesCount']); + $postFileCallback(count($json['files'])); + } + + if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) { + $peakMemoryUsages[$processIdentifier] = $json['memoryUsage']; } $internalErrorsCount += $json['internalErrorsCount']; @@ -182,35 +302,50 @@ public function analyse( $job = array_pop($jobs); $process->request(['action' => 'analyse', 'files' => $job]); - }, $handleError, function ($exitCode, string $output) use (&$internalErrors, &$internalErrorsCount, $processIdentifier): void { - $this->processPool->tryQuitProcess($processIdentifier); + }, $handleError, function ($exitCode, string $output) use (&$someChildEnded, &$peakMemoryUsages, &$internalErrors, &$internalErrorsCount, $processIdentifier): void { + if ($someChildEnded === false) { + $peakMemoryUsages['main'] = memory_get_usage(true); + } + $someChildEnded = true; + if ($exitCode === 0) { + $this->processPool->tryQuitProcess($processIdentifier); return; } if ($exitCode === null) { + $this->processPool->tryQuitProcess($processIdentifier); + return; + } + + $memoryLimitMessage = 'PHPStan process crashed because it reached configured PHP memory limit'; + if (str_contains($output, $memoryLimitMessage)) { + foreach ($internalErrors as $internalError) { + if (!str_contains($internalError->getMessage(), $memoryLimitMessage)) { + continue; + } + + $this->processPool->tryQuitProcess($processIdentifier); + return; + } + $internalErrors[] = new InternalError(sprintf( + "Child process error: %s: %s\n%s\n", + $memoryLimitMessage, + ini_get('memory_limit'), + 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', + ), 'running parallel worker', [], null, false); + $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); return; } - $internalErrors[] = sprintf('Child process error (exit code %d): %s', $exitCode, $output); + $internalErrors[] = new InternalError(sprintf('Child process error (exit code %d): %s', $exitCode, $output), 'running parallel worker', [], null, true); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); }); $this->processPool->attachProcess($processIdentifier, $process); } - $loop->run(); - - if (count($jobs) > 0 && $internalErrorsCount === 0) { - $internalErrors[] = 'Some parallel worker jobs have not finished.'; - $internalErrorsCount++; - } - - return new AnalyserResult( - $errors, - $internalErrors, - $internalErrorsCount === 0 ? $dependencies : null, - $exportedNodes, - $reachedInternalErrorsCountLimit - ); + return $deferred->promise(); } } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index e44cc6d1f2..36799c274d 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -2,23 +2,24 @@ 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 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; @@ -29,40 +30,37 @@ class Process /** @var callable(mixed[] $json) : void */ private $onData; - /** @var callable(\Throwable $exception) : void */ + /** @var callable(Throwable $exception): void */ private $onError; private ?TimerInterface $timer = null; public function __construct( - string $command, - LoopInterface $loop, - float $timeoutSeconds + private string $command, + private LoopInterface $loop, + private float $timeoutSeconds, ) { - $this->command = $command; - $this->loop = $loop; - $this->timeoutSeconds = $timeoutSeconds; } /** * @param callable(mixed[] $json) : void $onData - * @param callable(\Throwable $exception) : void $onError + * @param callable(Throwable $exception): void $onError * @param callable(?int $exitCode, string $output) : void $onExit */ public function start(callable $onData, callable $onError, callable $onExit): void { $tmpStdOut = tmpfile(); if ($tmpStdOut === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stdout.'); + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); } $tmpStdErr = tmpfile(); if ($tmpStdErr === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stderr.'); + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); } $this->stdOut = $tmpStdOut; $this->stdErr = $tmpStdErr; - $this->process = new \React\ChildProcess\Process($this->command, null, null, [ + $this->process = new \React\ChildProcess\Process($this->command, fds: [ 1 => $this->stdOut, 2 => $this->stdErr, ]); @@ -74,16 +72,11 @@ public function start(callable $onData, callable $onError, callable $onExit): vo $output = ''; rewind($this->stdOut); - $stdOut = stream_get_contents($this->stdOut); - if (is_string($stdOut)) { - $output .= $stdOut; - } + $output .= stream_get_contents($this->stdOut); rewind($this->stdErr); - $stdErr = stream_get_contents($this->stdErr); - if (is_string($stdErr)) { - $output .= $stdErr; - } + $output .= stream_get_contents($this->stdErr); + $onExit($exitCode, $output); fclose($this->stdOut); fclose($this->stdErr); @@ -106,10 +99,13 @@ private function cancelTimer(): void public function request(array $data): void { $this->cancelTimer(); + if ($this->in === null) { + throw new ShouldNotHappenException(); + } $this->in->write($data); $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { $onError = $this->onError; - $onError(new \Exception(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); + $onError(new ProcessTimedOutException(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); }); } @@ -124,6 +120,10 @@ public function quit(): void $pipe->close(); } + if ($this->in === null) { + return; + } + $this->in->end(); } @@ -139,11 +139,11 @@ public function bindConnection(ReadableStreamInterface $out, WritableStreamInter $onData($json['result']); }); $this->in = $in; - $out->on('error', function (\Throwable $error): void { + $out->on('error', function (Throwable $error): void { $onError = $this->onError; $onError($error); }); - $in->on('error', function (\Throwable $error): void { + $in->on('error', function (Throwable $error): void { $onError = $this->onError; $onError($error); }); diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index a411712cef..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]; @@ -51,6 +59,8 @@ private function quitProcess(string $identifier): void } $this->server->close(); + $callback = $this->onServerClose; + $callback(); } public function quitAll(): void diff --git a/src/Parallel/ProcessTimedOutException.php b/src/Parallel/ProcessTimedOutException.php new file mode 100644 index 0000000000..50667999a1 --- /dev/null +++ b/src/Parallel/ProcessTimedOutException.php @@ -0,0 +1,10 @@ +> */ - private array $jobs; - /** - * @param int $numberOfProcesses * @param array> $jobs */ - public function __construct(int $numberOfProcesses, array $jobs) + public function __construct(private int $numberOfProcesses, private array $jobs) { - $this->numberOfProcesses = $numberOfProcesses; - $this->jobs = $jobs; } public function getNumberOfProcesses(): int diff --git a/src/Parallel/Scheduler.php b/src/Parallel/Scheduler.php index 1a3e6126d5..239db76681 100644 --- a/src/Parallel/Scheduler.php +++ b/src/Parallel/Scheduler.php @@ -2,43 +2,79 @@ namespace PHPStan\Parallel; -class Scheduler -{ - - private int $jobSize; +use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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; +#[AutowiredService] +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 + #[AutowiredParameter(ref: '%parallel.jobSize%')] + private int $jobSize, + #[AutowiredParameter(ref: '%parallel.maximumNumberOfProcesses%')] + private int $maximumNumberOfProcesses, + #[AutowiredParameter(ref: '%parallel.minimumNumberOfJobsPerProcess%')] + private int $minimumNumberOfJobsPerProcess, ) { - $this->jobSize = $jobSize; - $this->maximumNumberOfProcesses = $maximumNumberOfProcesses; - $this->minimumNumberOfJobsPerProcess = $minimumNumberOfJobsPerProcess; } /** - * @param int $cpuCores * @param array $files - * @return Schedule */ public function scheduleWork( int $cpuCores, - array $files + array $files, ): Schedule { $jobs = array_chunk($files, $this->jobSize); $numberOfProcesses = min( max((int) floor(count($jobs) / $this->minimumNumberOfJobsPerProcess), 1), - $cpuCores + $cpuCores, ); - return new Schedule(min($numberOfProcesses, $this->maximumNumberOfProcesses), $jobs); + $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..77e9840633 --- /dev/null +++ b/src/Parser/AnonymousClassVisitor.php @@ -0,0 +1,58 @@ +> */ + private array $nodesPerLine = []; + + #[Override] + public function beforeTraverse(array $nodes): ?array + { + $this->nodesPerLine = []; + return null; + } + + #[Override] + 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; + } + + #[Override] + 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..af5c2e41fb --- /dev/null +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -0,0 +1,31 @@ +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..1aaf664f1c --- /dev/null +++ b/src/Parser/ArrayFindArgVisitor.php @@ -0,0 +1,32 @@ +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..c9e5b8a358 --- /dev/null +++ b/src/Parser/ArrayMapArgVisitor.php @@ -0,0 +1,76 @@ +isArrayMapCall($node)) { + return null; + } + + $args = $node->getArgs(); + if (count($args) < 2) { + return null; + } + + $callbackArg = null; + $arrayArgs = []; + 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; + } + } + + $arrayArgs[] = $arg; + } + + if ($callbackArg !== null) { + $callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs); + return new Node\Expr\FuncCall( + $node->name, + [$callbackArg, ...$arrayArgs], + $node->getAttributes(), + ); + } + + return null; + } + + /** + * @phpstan-assert-if-true Node\Expr\FuncCall $node + */ + private function isArrayMapCall(Node $node): bool + { + if (!$node instanceof Node\Expr\FuncCall) { + return false; + } + if (!$node->name instanceof Node\Name) { + return false; + } + if ($node->isFirstClassCallable()) { + return false; + } + + return $node->name->toLowerString() === 'array_map'; + } + +} diff --git a/src/Parser/ArrayWalkArgVisitor.php b/src/Parser/ArrayWalkArgVisitor.php new file mode 100644 index 0000000000..f4a0a05022 --- /dev/null +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -0,0 +1,31 @@ +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..948f48625d --- /dev/null +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -0,0 +1,45 @@ +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 f78bfc60f1..400c21bf5a 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -2,35 +2,31 @@ namespace PHPStan\Parser; +use PhpParser\Node; use PHPStan\File\FileReader; +use function array_slice; -class CachedParser implements Parser +final class CachedParser implements Parser { - private \PHPStan\Parser\Parser $originalParser; - - /** @var array*/ + /** @var array*/ private array $cachedNodesByString = []; private int $cachedNodesByStringCount = 0; - private int $cachedNodesByStringCountMax; - /** @var array */ private array $parsedByString = []; public function __construct( - Parser $originalParser, - int $cachedNodesByStringCountMax + private Parser $originalParser, + private int $cachedNodesByStringCountMax, ) { - $this->originalParser = $originalParser; - $this->cachedNodesByStringCountMax = $cachedNodesByStringCountMax; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { @@ -38,8 +34,7 @@ public function parseFile(string $file): array $this->cachedNodesByString = array_slice( $this->cachedNodesByString, 1, - null, - true + preserve_keys: true, ); --$this->cachedNodesByStringCount; @@ -56,8 +51,7 @@ public function parseFile(string $file): array } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { @@ -65,8 +59,7 @@ public function parseString(string $sourceCode): array $this->cachedNodesByString = array_slice( $this->cachedNodesByString, 1, - null, - true + preserve_keys: true, ); --$this->cachedNodesByStringCount; @@ -92,7 +85,7 @@ public function getCachedNodesByStringCountMax(): int } /** - * @return array + * @return array */ public function getCachedNodesByString(): array { diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php new file mode 100644 index 0000000000..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..c0f4c0b458 --- /dev/null +++ b/src/Parser/CleaningVisitor.php @@ -0,0 +1,106 @@ +nodeFinder = new NodeFinder(); + } + + #[Override] + 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..30533190b5 --- /dev/null +++ b/src/Parser/ClosureArgVisitor.php @@ -0,0 +1,45 @@ +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..ed94333cad --- /dev/null +++ b/src/Parser/ClosureBindArgVisitor.php @@ -0,0 +1,37 @@ +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..a94de9f6ef --- /dev/null +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -0,0 +1,34 @@ +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..16111b34fd --- /dev/null +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -0,0 +1,31 @@ +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..11f8ebb00e --- /dev/null +++ b/src/Parser/DeclarePositionVisitor.php @@ -0,0 +1,49 @@ +isFirstStatement = true; + return null; + } + + #[Override] + public function enterNode(Node $node): ?Node + { + // ignore shebang + if ( + $this->isFirstStatement + && $node instanceof Node\Stmt\InlineHTML + && str_starts_with($node->value, '#!') + ) { + return null; + } + + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Declare_) { + $node->setAttribute(self::ATTRIBUTE_NAME, $this->isFirstStatement); + } + + $this->isFirstStatement = false; + } + + return null; + } + +} diff --git a/src/Parser/FunctionCallStatementFinder.php b/src/Parser/FunctionCallStatementFinder.php deleted file mode 100644 index 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..c1d5c9e247 --- /dev/null +++ b/src/Parser/ImmediatelyInvokedClosureVisitor.php @@ -0,0 +1,26 @@ +name instanceof Node\Expr\Closure) { + $node->name->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/ImplodeArgVisitor.php b/src/Parser/ImplodeArgVisitor.php new file mode 100644 index 0000000000..36e0201b83 --- /dev/null +++ b/src/Parser/ImplodeArgVisitor.php @@ -0,0 +1,32 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if (in_array($functionName, ['implode', 'join'], true)) { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->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..f122206e0b --- /dev/null +++ b/src/Parser/LastConditionVisitor.php @@ -0,0 +1,97 @@ +elseifs !== []) { + $lastElseIf = count($node->elseifs) - 1; + + $elseIsMissingOrThrowing = $node->else === null + || ( + count($node->else->stmts) === 1 + && $node->else->stmts[0] instanceof Node\Stmt\Expression + && $node->else->stmts[0]->expr instanceof Node\Expr\Throw_ + ); + + foreach ($node->elseifs as $i => $elseif) { + $isLast = $i === $lastElseIf && $elseIsMissingOrThrowing; + $elseif->cond->setAttribute(self::ATTRIBUTE_NAME, $isLast); + } + } + + if ($node instanceof Node\Expr\Match_ && $node->arms !== []) { + $lastArm = count($node->arms) - 1; + + foreach ($node->arms as $i => $arm) { + if ($arm->conds === null || $arm->conds === []) { + continue; + } + + $isLast = $i === $lastArm; + $index = count($arm->conds) - 1; + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_NAME, $isLast); + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_IS_MATCH_NAME, true); + } + } + + if ( + $node instanceof Node\Stmt\Function_ + || $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Stmt\If_ + || $node instanceof Node\Stmt\ElseIf_ + || $node instanceof Node\Stmt\Else_ + || $node instanceof Node\Stmt\Case_ + || $node instanceof Node\Stmt\Catch_ + || $node instanceof Node\Stmt\Do_ + || $node instanceof Node\Stmt\Finally_ + || $node instanceof Node\Stmt\For_ + || $node instanceof Node\Stmt\Foreach_ + || $node instanceof Node\Stmt\Namespace_ + || $node instanceof Node\Stmt\TryCatch + || $node instanceof Node\Stmt\While_ + ) { + $statements = $node->stmts ?? []; + $statementCount = count($statements); + + if ($statementCount < 2) { + return null; + } + + $lastStatement = $statements[$statementCount - 1]; + + if (!$lastStatement instanceof Node\Stmt\Expression) { + return null; + } + + if (!$lastStatement->expr instanceof Node\Expr\Throw_) { + return null; + } + + if (!$statements[$statementCount - 2] instanceof Node\Stmt\If_ || $statements[$statementCount - 2]->else !== null) { + return null; + } + + $if = $statements[$statementCount - 2]; + $cond = count($if->elseifs) > 0 ? $if->elseifs[count($if->elseifs) - 1]->cond : $if->cond; + $cond->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/LexerFactory.php b/src/Parser/LexerFactory.php index e7b8dae7ab..1f844ff7cd 100644 --- a/src/Parser/LexerFactory.php +++ b/src/Parser/LexerFactory.php @@ -3,28 +3,30 @@ namespace PHPStan\Parser; use PhpParser\Lexer; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; +use const PHP_VERSION_ID; -class LexerFactory +#[AutowiredService] +final class LexerFactory { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function create(): Lexer { - $options = ['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]; if ($this->phpVersion->getVersionId() === PHP_VERSION_ID) { - return new Lexer($options); + return new Lexer(); } - $options['phpVersion'] = $this->phpVersion->getVersionString(); + return new Lexer\Emulative(\PhpParser\PhpVersion::fromString($this->phpVersion->getVersionString())); + } - return new Lexer\Emulative($options); + public function createEmulative(): Lexer\Emulative + { + return new Lexer\Emulative(); } } diff --git a/src/Parser/LineAttributesVisitor.php b/src/Parser/LineAttributesVisitor.php new file mode 100644 index 0000000000..f664f1dc88 --- /dev/null +++ b/src/Parser/LineAttributesVisitor.php @@ -0,0 +1,30 @@ +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..5966f7f8e6 --- /dev/null +++ b/src/Parser/MagicConstantParamDefaultVisitor.php @@ -0,0 +1,25 @@ +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..60fd8fd72e --- /dev/null +++ b/src/Parser/NewAssignedToPropertyVisitor.php @@ -0,0 +1,30 @@ +var instanceof Node\Expr\PropertyFetch || $node->var instanceof Node\Expr\StaticPropertyFetch) + && $node->expr instanceof Node\Expr\New_ + ) { + $node->expr->setAttribute(self::ATTRIBUTE_NAME, $node->var); + } + } + return null; + } + +} diff --git a/src/Parser/NodeList.php b/src/Parser/NodeList.php deleted file mode 100644 index 18b52a456e..0000000000 --- a/src/Parser/NodeList.php +++ /dev/null @@ -1,43 +0,0 @@ -node = $node; - $this->next = $next; - } - - public function append(Node $node): self - { - $current = $this; - while ($current->next !== null) { - $current = $current->next; - } - - $new = new self($node); - $current->next = $new; - - return $new; - } - - public function getNode(): Node - { - return $this->node; - } - - public function getNext(): ?self - { - return $this->next; - } - -} diff --git a/src/Parser/ParentStmtTypesVisitor.php b/src/Parser/ParentStmtTypesVisitor.php new file mode 100644 index 0000000000..faaa4024ee --- /dev/null +++ b/src/Parser/ParentStmtTypesVisitor.php @@ -0,0 +1,56 @@ +> */ + private array $typeStack = []; + + #[Override] + public function beforeTraverse(array $nodes): ?array + { + $this->typeStack = []; + return null; + } + + #[Override] + 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; + } + + #[Override] + 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 1346facd1d..187d6875c6 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -2,21 +2,22 @@ namespace PHPStan\Parser; +use PhpParser\Node; + /** @api */ interface Parser { /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] - * @throws \PHPStan\Parser\ParserErrorsException + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseFile(string $file): array; /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] - * @throws \PHPStan\Parser\ParserErrorsException + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseString(string $sourceCode): array; diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index 5be367a751..d68d18220a 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -2,34 +2,36 @@ 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; - - private ?string $parsedFile; + /** @var mixed[] */ + private array $attributes; /** - * @param \PhpParser\Error[] $errors - * @param string|null $parsedFile + * @param Error[] $errors */ public function __construct( - array $errors, - ?string $parsedFile + private array $errors, + private ?string $parsedFile, ) { - parent::__construct(implode(', ', array_map(static function (Error $error): string { - return $error->getMessage(); - }, $errors))); - $this->errors = $errors; - $this->parsedFile = $parsedFile; + parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getRawMessage(), $errors))); + if (count($errors) > 0) { + $this->attributes = $errors[0]->getAttributes(); + } else { + $this->attributes = []; + } } /** - * @return \PhpParser\Error[] + * @return Error[] */ public function getErrors(): array { @@ -41,4 +43,12 @@ public function getParsedFile(): ?string return $this->parsedFile; } + /** + * @return mixed[] + */ + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 7fa5f6aba3..53040aa27f 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -3,37 +3,75 @@ namespace PHPStan\Parser; use PHPStan\File\FileHelper; +use function array_fill_keys; +use function array_slice; +use function count; +use function explode; +use function implode; +use function is_link; +use function realpath; +use function str_contains; +use const DIRECTORY_SEPARATOR; -class PathRoutingParser implements Parser +final class PathRoutingParser implements Parser { - private FileHelper $fileHelper; + private ?string $singleReflectionFile; - private Parser $currentPhpVersionRichParser; - - private Parser $currentPhpVersionSimpleParser; - - private Parser $php8Parser; + /** @var array filePath(string) => bool(true) */ + private array $analysedFiles = []; public function __construct( - FileHelper $fileHelper, - Parser $currentPhpVersionRichParser, - Parser $currentPhpVersionSimpleParser, - Parser $php8Parser + private FileHelper $fileHelper, + private Parser $currentPhpVersionRichParser, + private Parser $currentPhpVersionSimpleParser, + private Parser $php8Parser, + ?string $singleReflectionFile, ) { - $this->fileHelper = $fileHelper; - $this->currentPhpVersionRichParser = $currentPhpVersionRichParser; - $this->currentPhpVersionSimpleParser = $currentPhpVersionSimpleParser; - $this->php8Parser = $php8Parser; + $this->singleReflectionFile = $singleReflectionFile !== null ? $fileHelper->normalizePath($singleReflectionFile) : null; + } + + /** + * @param string[] $files + */ + public function setAnalysedFiles(array $files): void + { + $this->analysedFiles = array_fill_keys($files, true); } public function parseFile(string $file): array { - $file = $this->fileHelper->normalizePath($file, '/'); - if (strpos($file, 'vendor/jetbrains/phpstorm-stubs') !== false) { + $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]) && $file !== $this->singleReflectionFile) { + // 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); } diff --git a/src/Parser/PhpParserDecorator.php b/src/Parser/PhpParserDecorator.php index 6719215d69..a8a2102f33 100644 --- a/src/Parser/PhpParserDecorator.php +++ b/src/Parser/PhpParserDecorator.php @@ -2,34 +2,42 @@ namespace PHPStan\Parser; +use Override; +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[] */ + #[Override] public function parse(string $code, ?ErrorHandler $errorHandler = null): array { try { return $this->wrappedParser->parseString($code); - } catch (\PHPStan\Parser\ParserErrorsException $e) { + } catch (ParserErrorsException $e) { $message = $e->getMessage(); if ($e->getParsedFile() !== null) { $message .= sprintf(' in file %s', $e->getParsedFile()); } - throw new \PhpParser\Error($message); + throw new Error($message, $e->getAttributes()); } } + #[Override] + 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..6807d9ccaa --- /dev/null +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -0,0 +1,100 @@ +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 index 2a5fc5d56c..a50ec45dbe 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -3,71 +3,344 @@ namespace PHPStan\Parser; use PhpParser\ErrorHandler\Collecting; +use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; -use PhpParser\NodeVisitor\NodeConnectingVisitor; +use PhpParser\Token; +use PHPStan\Analyser\Ignore\IgnoreLexer; +use PHPStan\Analyser\Ignore\IgnoreParseException; +use PHPStan\DependencyInjection\Container; use PHPStan\File\FileReader; -use PHPStan\NodeVisitor\StatementOrderVisitor; +use PHPStan\ShouldNotHappenException; +use function array_filter; +use function array_map; +use function count; +use function implode; +use function in_array; +use function preg_match_all; +use function sprintf; +use function str_contains; +use function strlen; +use function strpos; +use function substr; +use function substr_count; +use const ARRAY_FILTER_USE_KEY; +use const PREG_OFFSET_CAPTURE; +use const T_COMMENT; +use const T_DOC_COMMENT; +use const T_WHITESPACE; -class RichParser implements Parser +final class RichParser implements Parser { - private \PhpParser\Parser $parser; + public const VISITOR_SERVICE_TAG = 'phpstan.parser.richParserNodeVisitor'; - private NameResolver $nameResolver; + private const PHPDOC_TAG_REGEX = '(@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+)'; - private NodeConnectingVisitor $nodeConnectingVisitor; - - private StatementOrderVisitor $statementOrderVisitor; + private const PHPDOC_DOCTRINE_TAG_REGEX = '(@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*)'; public function __construct( - \PhpParser\Parser $parser, - NameResolver $nameResolver, - NodeConnectingVisitor $nodeConnectingVisitor, - StatementOrderVisitor $statementOrderVisitor + private \PhpParser\Parser $parser, + private NameResolver $nameResolver, + private Container $container, + private IgnoreLexer $ignoreLexer, ) { - $this->parser = $parser; - $this->nameResolver = $nameResolver; - $this->nodeConnectingVisitor = $nodeConnectingVisitor; - $this->statementOrderVisitor = $statementOrderVisitor; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { try { return $this->parseString(FileReader::read($file)); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - throw new \PHPStan\Parser\ParserErrorsException($e->getErrors(), $file); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); } } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); + + $tokens = $this->parser->getTokens(); if ($errorHandler->hasErrors()) { - throw new \PHPStan\Parser\ParserErrorsException($errorHandler->getErrors(), null); + throw new ParserErrorsException($errorHandler->getErrors(), null); } if ($nodes === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); - $nodeTraverser->addVisitor($this->nodeConnectingVisitor); - $nodeTraverser->addVisitor($this->statementOrderVisitor); - /** @var array<\PhpParser\Node\Stmt> */ - return $nodeTraverser->traverse($nodes); + $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 index f20603457b..8fbd112742 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -3,58 +3,57 @@ namespace PHPStan\Parser; use PhpParser\ErrorHandler\Collecting; +use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PHPStan\File\FileReader; +use PHPStan\ShouldNotHappenException; -class SimpleParser implements Parser +final class SimpleParser implements Parser { - private \PhpParser\Parser $parser; - - private NameResolver $nameResolver; - public function __construct( - \PhpParser\Parser $parser, - NameResolver $nameResolver + private \PhpParser\Parser $parser, + private NameResolver $nameResolver, + private VariadicMethodsVisitor $variadicMethodsVisitor, + private VariadicFunctionsVisitor $variadicFunctionsVisitor, ) { - $this->parser = $parser; - $this->nameResolver = $nameResolver; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { try { return $this->parseString(FileReader::read($file)); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - throw new \PHPStan\Parser\ParserErrorsException($e->getErrors(), $file); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); } } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); if ($errorHandler->hasErrors()) { - throw new \PHPStan\Parser\ParserErrorsException($errorHandler->getErrors(), null); + throw new ParserErrorsException($errorHandler->getErrors(), null); } if ($nodes === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); + $nodeTraverser->addVisitor($this->variadicMethodsVisitor); + $nodeTraverser->addVisitor($this->variadicFunctionsVisitor); - /** @var array<\PhpParser\Node\Stmt> */ + /** @var array */ return $nodeTraverser->traverse($nodes); } diff --git a/src/Parser/StandaloneThrowExprVisitor.php b/src/Parser/StandaloneThrowExprVisitor.php new file mode 100644 index 0000000000..e517701e10 --- /dev/null +++ b/src/Parser/StandaloneThrowExprVisitor.php @@ -0,0 +1,32 @@ +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..5c043161b5 --- /dev/null +++ b/src/Parser/TraitCollectingVisitor.php @@ -0,0 +1,27 @@ + */ + public array $traits = []; + + #[Override] + 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..df2c8f4210 --- /dev/null +++ b/src/Parser/TryCatchTypeVisitor.php @@ -0,0 +1,80 @@ +|null> */ + private array $typeStack = []; + + #[Override] + public function beforeTraverse(array $nodes): ?array + { + $this->typeStack = []; + return null; + } + + #[Override] + 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; + } + + #[Override] + 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..3ad2fa0f11 --- /dev/null +++ b/src/Parser/TypeTraverserInstanceofVisitor.php @@ -0,0 +1,62 @@ +depth = 0; + return null; + } + + #[Override] + 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; + } + + #[Override] + 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..70d19bacec --- /dev/null +++ b/src/Parser/VariadicFunctionsVisitor.php @@ -0,0 +1,99 @@ + */ + public static array $cache = []; + + /** @var array */ + private array $variadicFunctions = []; + + public const ATTRIBUTE_NAME = 'variadicFunctions'; + + #[Override] + public function beforeTraverse(array $nodes): ?array + { + $this->topNode = null; + $this->variadicFunctions = []; + $this->inNamespace = null; + $this->inFunction = null; + + return null; + } + + #[Override] + public function enterNode(Node $node): ?Node + { + $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; + } + + #[Override] + 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; + } + + #[Override] + 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..e82e6dfb6b --- /dev/null +++ b/src/Parser/VariadicMethodsVisitor.php @@ -0,0 +1,144 @@ + */ + private array $classStack = []; + + /** @var array */ + private array $inMethodStack = []; + + /** @var array> */ + public static array $cache = []; + + /** @var array> */ + private array $variadicMethods = []; + + #[Override] + public function beforeTraverse(array $nodes): ?array + { + $this->topNode = null; + $this->variadicMethods = []; + $this->inNamespace = null; + $this->classStack = []; + $this->inMethodStack = []; + + return null; + } + + #[Override] + public function enterNode(Node $node): ?Node + { + $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->inMethodStack[] = $node->name->name; + } + + $lastMethod = $this->inMethodStack[count($this->inMethodStack) - 1] ?? null; + + if ( + $lastMethod !== 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($lastMethod, $this->variadicMethods[$lastClass]) + ) { + $this->variadicMethods[$lastClass][$lastMethod] = true; + } + } + + } + + return null; + } + + #[Override] + public function leaveNode(Node $node): ?Node + { + if ($node instanceof ClassMethod) { + $lastClass = $this->classStack[count($this->classStack) - 1] ?? null; + $lastMethod = $this->inMethodStack[count($this->inMethodStack) - 1] ?? null; + if ($lastClass !== null && $lastMethod !== null) { + $this->variadicMethods[$lastClass][$lastMethod] ??= false; + } + array_pop($this->inMethodStack); + } + + if ($node instanceof Node\Stmt\ClassLike) { + array_pop($this->classStack); + } + + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = null; + } + + return null; + } + + #[Override] + 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..924bfde5db --- /dev/null +++ b/src/Php/ComposerPhpVersionFactory.php @@ -0,0 +1,125 @@ +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 cbb75add69..2a151b58ae 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -2,15 +2,50 @@ namespace PHPStan\Php; -/** @api */ -class PhpVersion +use PHPStan\DependencyInjection\AutowiredService; +use function floor; + +/** + * @api + */ +#[AutowiredService(factory: '@PHPStan\Php\PhpVersionFactory::create')] +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; + + /** + * @api + * + * @param self::SOURCE_* $source + */ + public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN) + { + } - public function __construct(int $versionId) + /** + * @return self::SOURCE_* + */ + public function getSource(): int { - $this->versionId = $versionId; + 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 @@ -18,11 +53,26 @@ public function getVersionId(): int return $this->versionId; } + public function getMajorVersionId(): int + { + return (int) floor($this->versionId / 10000); + } + + public function getMinorVersionId(): int + { + return (int) floor(($this->versionId % 10000) / 100); + } + + public function getPatchVersionId(): int + { + return (int) floor($this->versionId % 100); + } + public function getVersionString(): string { - $first = (int) floor($this->versionId / 10000); - $second = (int) floor(($this->versionId % 10000) / 100); - $third = (int) floor($this->versionId % 100); + $first = $this->getMajorVersionId(); + $second = $this->getMinorVersionId(); + $third = $this->getPatchVersionId(); return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); } @@ -42,6 +92,11 @@ public function supportsReturnCovariance(): bool return $this->versionId >= 70400; } + public function supportsNoncapturingCatches(): bool + { + return $this->versionId >= 80000; + } + public function supportsNativeUnionTypes(): bool { return $this->versionId >= 80000; @@ -52,6 +107,16 @@ public function deprecatesRequiredParameterAfterOptional(): bool return $this->versionId >= 80000; } + public function deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull(): bool + { + return $this->versionId >= 80100; + } + + public function deprecatesRequiredParameterAfterOptionalUnionOrMixed(): bool + { + return $this->versionId >= 80300; + } + public function supportsLessOverridenParametersWithVariadic(): bool { return $this->versionId >= 80000; @@ -142,4 +207,211 @@ 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; + } + + public function hasPDOSubclasses(): bool + { + return $this->versionId >= 80400; + } + } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index 8d726313fd..73f510e0dc 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -2,39 +2,45 @@ namespace PHPStan\Php; +use PHPStan\DependencyInjection\AutowiredService; +use function explode; +use function max; +use function min; use const PHP_VERSION_ID; -class PhpVersionFactory +#[AutowiredService(factory: '@PHPStan\Php\PhpVersionFactoryFactory::create')] +final class PhpVersionFactory { - private ?int $versionId; - - private ?string $composerPhpVersion; + public const MIN_PHP_VERSION = 70100; + public const MAX_PHP_VERSION = 80599; + public const MAX_PHP5_VERSION = 50699; + public const MAX_PHP7_VERSION = 70499; public function __construct( - ?int $versionId, - ?string $composerPhpVersion + private ?int $versionId, + private ?string $composerPhpVersion, ) { - $this->versionId = $versionId; - $this->composerPhpVersion = $composerPhpVersion; } public function create(): PhpVersion { $versionId = $this->versionId; - if ($versionId === null && $this->composerPhpVersion !== null) { + if ($versionId !== null) { + $source = PhpVersion::SOURCE_CONFIG; + } elseif ($this->composerPhpVersion !== null) { $parts = explode('.', $this->composerPhpVersion); $tmp = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); - $tmp = max($tmp, 70100); - $versionId = min($tmp, 80099); - } - - if ($versionId === null) { + $tmp = max($tmp, self::MIN_PHP_VERSION); + $versionId = min($tmp, self::MAX_PHP_VERSION); + $source = PhpVersion::SOURCE_COMPOSER_PLATFORM_PHP; + } else { $versionId = PHP_VERSION_ID; + $source = PhpVersion::SOURCE_RUNTIME; } - return new PhpVersion($versionId); + return new PhpVersion($versionId, $source); } } diff --git a/src/Php/PhpVersionFactoryFactory.php b/src/Php/PhpVersionFactoryFactory.php index f301adb29b..6ecfed8c71 100644 --- a/src/Php/PhpVersionFactoryFactory.php +++ b/src/Php/PhpVersionFactoryFactory.php @@ -3,26 +3,33 @@ namespace PHPStan\Php; use Nette\Utils\Json; +use Nette\Utils\JsonException; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; +use function count; +use function end; +use function is_array; +use function is_file; +use function is_int; +use function is_string; -class PhpVersionFactoryFactory +#[AutowiredService] +final class PhpVersionFactoryFactory { - private ?int $versionId; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** + * @param int|array{min: int, max: int}|null $phpVersion * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - ?int $versionId, - array $composerAutoloaderProjectPaths + #[AutowiredParameter] + private int|array|null $phpVersion, + #[AutowiredParameter] + private array $composerAutoloaderProjectPaths, ) { - $this->versionId = $versionId; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } public function create(): PhpVersionFactory @@ -38,13 +45,23 @@ public function create(): PhpVersionFactory if (is_string($platformVersion)) { $composerPhpVersion = $platformVersion; } - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { // pass } } } - return new PhpVersionFactory($this->versionId, $composerPhpVersion); + $versionId = null; + + if (is_int($this->phpVersion)) { + $versionId = $this->phpVersion; + } + + if (is_array($this->phpVersion)) { + $versionId = $this->phpVersion['min']; + } + + return new PhpVersionFactory($versionId, $composerPhpVersion); } } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php new file mode 100644 index 0000000000..cc2ea94c98 --- /dev/null +++ b/src/Php/PhpVersions.php @@ -0,0 +1,51 @@ +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; + } + + public function supportsTrueAndFalseStandaloneType(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; + } + +} diff --git a/src/PhpDoc/ConstExprNodeResolver.php b/src/PhpDoc/ConstExprNodeResolver.php index 8a22e2da77..7d1b0ad824 100644 --- a/src/PhpDoc/ConstExprNodeResolver.php +++ b/src/PhpDoc/ConstExprNodeResolver.php @@ -2,6 +2,8 @@ namespace PHPStan\PhpDoc; +use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -10,60 +12,130 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\ConstantTypeHelper; -use PHPStan\Type\MixedType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\ErrorType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function strtolower; -class ConstExprNodeResolver +#[AutowiredService] +final class ConstExprNodeResolver { - public function resolve(ConstExprNode $node): Type + public function __construct( + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + + public function resolve(ConstExprNode $node, NameScope $nameScope): Type { if ($node instanceof ConstExprArrayNode) { - return $this->resolveArrayNode($node); + return $this->resolveArrayNode($node, $nameScope); } if ($node instanceof ConstExprFalseNode) { - return ConstantTypeHelper::getTypeFromValue(false); + return new ConstantBooleanType(false); } if ($node instanceof ConstExprTrueNode) { - return ConstantTypeHelper::getTypeFromValue(true); + return new ConstantBooleanType(true); } if ($node instanceof ConstExprFloatNode) { - return ConstantTypeHelper::getTypeFromValue((float) $node->value); + return new ConstantFloatType((float) $node->value); } if ($node instanceof ConstExprIntegerNode) { - return ConstantTypeHelper::getTypeFromValue((int) $node->value); + return new ConstantIntegerType((int) $node->value); } if ($node instanceof ConstExprNullNode) { - return ConstantTypeHelper::getTypeFromValue(null); + return new NullType(); } if ($node instanceof ConstExprStringNode) { - return ConstantTypeHelper::getTypeFromValue($node->value); + return new ConstantStringType($node->value); } - return new MixedType(); + if ($node instanceof ConstFetchNode) { + if ($nameScope->getClassName() !== null) { + switch (strtolower($node->className)) { + case 'static': + case 'self': + $className = $nameScope->getClassName(); + break; + + case 'parent': + if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + if ($classReflection->getParentClass() === null) { + return new ErrorType(); + + } + + $className = $classReflection->getParentClass()->getName(); + } + break; + } + } + if (!isset($className)) { + $className = $nameScope->resolveStringName($node->className); + } + if (!$this->getReflectionProvider()->hasClass($className)) { + return new ErrorType(); + } + $classReflection = $this->getReflectionProvider()->getClass($className); + if (!$classReflection->hasConstant($node->name)) { + return new ErrorType(); + } + if ($classReflection->isEnum() && $classReflection->hasEnumCase($node->name)) { + return new EnumCaseObjectType($classReflection->getName(), $node->name); + } + + $reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($node->name); + if ($reflectionConstant === false) { + return new ErrorType(); + } + $declaringClass = $reflectionConstant->getDeclaringClass(); + + return $this->initializerExprTypeResolver->getType( + $reflectionConstant->getValueExpression(), + InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null), + ); + } + + return new ErrorType(); } - private function resolveArrayNode(ConstExprArrayNode $node): Type + private function resolveArrayNode(ConstExprArrayNode $node, NameScope $nameScope): Type { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($node->items as $item) { if ($item->key === null) { $key = null; } else { - $key = $this->resolve($item->key); + $key = $this->resolve($item->key, $nameScope); } - $arrayBuilder->setOffsetValueType($key, $this->resolve($item->value)); + $arrayBuilder->setOffsetValueType($key, $this->resolve($item->value, $nameScope)); } return $arrayBuilder->getArray(); } + private function getReflectionProvider(): ReflectionProvider + { + return $this->reflectionProviderProvider->getReflectionProvider(); + } + } diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php new file mode 100644 index 0000000000..69d7142ed9 --- /dev/null +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -0,0 +1,79 @@ +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..61ba6aca5d --- /dev/null +++ b/src/PhpDoc/JsonValidateStubFilesExtension.php @@ -0,0 +1,25 @@ +phpVersion->supportsJsonValidate()) { + return []; + } + + return [__DIR__ . '/../../stubs/json_validate.stub']; + } + +} diff --git a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php index f6cf2220ff..eb0a48b337 100644 --- a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php @@ -2,28 +2,25 @@ namespace PHPStan\PhpDoc; -class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider -{ +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Container; - private \PHPStan\DependencyInjection\Container $container; +#[AutowiredService(as: TypeNodeResolverExtensionRegistryProvider::class)] +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->container->getByType(TypeNodeResolver::class), - $this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG) - ); - } - - return $this->registry; + return $this->registry ??= new TypeNodeResolverExtensionAwareRegistry( + $this->container->getByType(TypeNodeResolver::class), + $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 3127628d9f..434770b42d 100644 --- a/src/PhpDoc/PhpDocBlock.php +++ b/src/PhpDoc/PhpDocBlock.php @@ -2,58 +2,35 @@ 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 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 +38,7 @@ public function getDocComment(): string return $this->docComment; } - public function getFile(): string + public function getFile(): ?string { return $this->file; } @@ -76,11 +53,6 @@ public function getTrait(): ?string return $this->trait; } - public function isExplicit(): bool - { - return $this->explicit; - } - /** * @return array */ @@ -107,163 +79,159 @@ 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, - ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames // unused + ?string $file, ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolvePropertyPhpDocBlockFromClass( + $parentReflection, + $propertyName, + ); + + 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, [], - [] + $docBlocksFromParents, ); } - /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $constantName - * @param string $file - * @param bool|null $explicit - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - * @return self - */ public static function resolvePhpDocBlockForConstant( ?string $docComment, ClassReflection $classReflection, - ?string $trait, // unused string $constantName, - string $file, - ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames // unused + ?string $file, ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveConstantPhpDocBlockFromClass( + $parentReflection, + $constantName, + ); + + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + return new self( + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $file, $classReflection, null, - $constantName, - $file, - 'hasConstant', - 'getConstant', - __FUNCTION__, - $explicit, [], - [] + $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( ?string $docComment, ClassReflection $classReflection, ?string $trait, string $methodName, - string $file, - ?bool $explicit, + ?string $file, array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): self { - return self::resolvePhpDocBlockTree( - $docComment, - $classReflection, - $trait, - $methodName, - $file, - 'hasNativeMethod', - 'getNativeMethod', - __FUNCTION__, - $explicit, - $originalPositionalParameterNames, - $newPositionalParameterNames - ); - } + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveMethodPhpDocBlockFromClass( + $parentReflection, + $methodName, + $newPositionalParameterNames, + ); - /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $name - * @param string $file - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool|null $explicit - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - * @return self - */ - private static function resolvePhpDocBlockTree( - ?string $docComment, - ClassReflection $classReflection, - ?string $trait, - string $name, - string $file, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, - ?bool $explicit, - array $originalPositionalParameterNames, - array $newPositionalParameterNames - ): self - { - $docBlocksFromParents = self::resolveParentPhpDocBlocks( - $classReflection, - $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, - $explicit ?? $docComment !== null, - $newPositionalParameterNames - ); + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + foreach ($classReflection->getTraits(true) as $traitReflection) { + if (!$traitReflection->hasNativeMethod($methodName)) { + continue; + } + $traitMethod = $traitReflection->getNativeMethod($methodName); + $abstract = $traitMethod->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + continue; + } + } elseif (!$abstract->yes()) { + continue; + } + + $methodVariant = $traitMethod->getOnlyVariant(); + $positionalMethodParameterNames = []; + foreach ($methodVariant->getParameters() as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); + } + + $docBlocksFromParents[] = new self( + $traitMethod->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection->getFileName(), + $classReflection, + $traitReflection->getName(), + self::remapParameterNames($newPositionalParameterNames, $positionalMethodParameterNames), + [], + ); + } return new self( - $docComment ?? '/** */', + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $file, $classReflection, $trait, - $explicit ?? true, self::remapParameterNames($originalPositionalParameterNames, $newPositionalParameterNames), - $docBlocksFromParents + $docBlocksFromParents, ); } @@ -274,7 +242,7 @@ private static function resolvePhpDocBlockTree( */ private static function remapParameterNames( array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): array { $parameterNameMapping = []; @@ -289,51 +257,6 @@ private static function remapParameterNames( } /** - * @param ClassReflection $classReflection - * @param string $name - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool $explicit - * @param array $positionalParameterNames - * @return array - */ - private static function resolveParentPhpDocBlocks( - ClassReflection $classReflection, - string $name, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, - bool $explicit, - array $positionalParameterNames - ): array - { - $result = []; - $parentReflections = self::getParentReflections($classReflection); - - foreach ($parentReflections as $parentReflection) { - $oneResult = self::resolvePhpDocBlockFromClass( - $parentReflection, - $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, - $explicit, - $positionalParameterNames - ); - - if ($oneResult === null) { // Null if it is private or from a wrong trait. - continue; - } - - $result[] = $oneResult; - } - - return $result; - } - - /** - * @param ClassReflection $classReflection * @return array */ private static function getParentReflections(ClassReflection $classReflection): array @@ -352,74 +275,108 @@ private static function getParentReflections(ClassReflection $classReflection): return $result; } + private static function resolveConstantPhpDocBlockFromClass( + ClassReflection $classReflection, + string $name, + ): ?self + { + if ($classReflection->hasConstant($name)) { + $parentReflection = $classReflection->getConstant($name); + if ($parentReflection->isPrivate()) { + return null; + } + + $classReflection = $parentReflection->getDeclaringClass(); + + return self::resolvePhpDocBlockForConstant( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection, + $name, + $classReflection->getFileName(), + ); + } + + return null; + } + + private static function resolvePropertyPhpDocBlockFromClass( + ClassReflection $classReflection, + string $name, + ): ?self + { + if ($classReflection->hasNativeProperty($name)) { + $parentReflection = $classReflection->getNativeProperty($name); + if ($parentReflection->isPrivate()) { + return null; + } + + $classReflection = $parentReflection->getDeclaringClass(); + $traitReflection = $parentReflection->getDeclaringTrait(); + + $trait = $traitReflection !== null + ? $traitReflection->getName() + : null; + + return self::resolvePhpDocBlockForProperty( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection, + $trait, + $name, + $classReflection->getFileName(), + ); + } + + 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|\PHPStan\Reflection\ConstantReflection $parentReflection */ - $parentReflection = $classReflection->$getMethodName($name); + if ($classReflection->hasNativeMethod($name)) { + $parentReflection = $classReflection->getNativeMethod($name); if ($parentReflection->isPrivate()) { return null; } - if ($parentReflection instanceof PhpPropertyReflection || $parentReflection instanceof ResolvedPropertyReflection) { + $classReflection = $parentReflection->getDeclaringClass(); + $traitReflection = null; + if ($parentReflection instanceof PhpMethodReflection || $parentReflection instanceof ResolvedMethodReflection) { $traitReflection = $parentReflection->getDeclaringTrait(); - $positionalMethodParameterNames = []; - } elseif ($parentReflection instanceof MethodReflection) { - $traitReflection = null; - if ($parentReflection instanceof PhpMethodReflection || $parentReflection instanceof ResolvedMethodReflection) { - $traitReflection = $parentReflection->getDeclaringTrait(); - } - $methodVariants = $parentReflection->getVariants(); - $positionalMethodParameterNames = []; - $lowercaseMethodName = strtolower($parentReflection->getName()); - if ( - count($methodVariants) === 1 - && $lowercaseMethodName !== '__construct' - && $lowercaseMethodName !== strtolower($parentReflection->getDeclaringClass()->getName()) - ) { - $methodParameters = $methodVariants[0]->getParameters(); - foreach ($methodParameters as $methodParameter) { - $positionalMethodParameterNames[] = $methodParameter->getName(); - } - } else { - $positionalMethodParameterNames = $positionalParameterNames; + } + $methodVariants = $parentReflection->getVariants(); + $positionalMethodParameterNames = []; + $lowercaseMethodName = strtolower($parentReflection->getName()); + if ( + count($methodVariants) === 1 + && $lowercaseMethodName !== '__construct' + && $lowercaseMethodName !== strtolower($parentReflection->getDeclaringClass()->getName()) + ) { + $methodParameters = $methodVariants[0]->getParameters(); + foreach ($methodParameters as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); } } else { - $traitReflection = null; - $positionalMethodParameterNames = []; + $positionalMethodParameterNames = $positionalParameterNames; } $trait = $traitReflection !== null ? $traitReflection->getName() : null; - return self::$resolveMethodName( - $parentReflection->getDocComment() ?? '/** */', + return self::resolvePhpDocBlockForMethod( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $classReflection, $trait, $name, - $classReflection->getFileNameWithPhpDocs(), - $explicit, + $classReflection->getFileName(), $positionalParameterNames, - $positionalMethodParameterNames + $positionalMethodParameterNames, ); } diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 9ad0cea135..b72e7fe36b 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -2,27 +2,30 @@ namespace PHPStan\PhpDoc; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function strtolower; -class PhpDocInheritanceResolver +#[AutowiredService] +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 + string $propertyName, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty( @@ -31,51 +34,38 @@ public function resolvePhpDocForProperty( null, $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 + ?string $classReflectionFileName, + string $constantName, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( $docComment, $classReflection, - null, $constantName, $classReflectionFileName, - null, - [], - [] ); - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, 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( @@ -84,49 +74,81 @@ public function resolvePhpDocForMethod( $declaringTraitName, $methodName, $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 ( - $parentPhpDocBlock->getClassReflection()->isBuiltin() - && $functionName !== null + $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 024f2489bc..ff91a44225 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -3,15 +3,24 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; +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\SealedTypeTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -24,41 +33,44 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\Reflection\PassedByReference; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; - -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; + +#[AutowiredService] +final class PhpDocNodeResolver { - private TypeNodeResolver $typeNodeResolver; - - private ConstExprNodeResolver $constExprNodeResolver; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - TypeNodeResolver $typeNodeResolver, - ConstExprNodeResolver $constExprNodeResolver, - UnresolvableTypeHelper $unresolvableTypeHelper + private TypeNodeResolver $typeNodeResolver, + private ConstExprNodeResolver $constExprNodeResolver, + private UnresolvableTypeHelper $unresolvableTypeHelper, ) { - $this->typeNodeResolver = $typeNodeResolver; - $this->constExprNodeResolver = $constExprNodeResolver; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array<(string|int), VarTag> */ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; $resolvedByTag = []; - foreach (['@var', '@psalm-var', '@phpstan-var'] as $tagName) { + foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) { $tagResolved = []; foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -67,9 +79,9 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar } if ($tagValue->variableName !== '') { $variableName = substr($tagValue->variableName, 1); - $resolved[$variableName] = new VarTag($type); + $resolved[$variableName] = new VarTag($type, true); } else { - $varTag = new VarTag($type); + $varTag = new VarTag($type, true); $tagResolved[] = $varTag; } } @@ -89,61 +101,93 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; - foreach ($phpDocNode->getPropertyTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property', '@phpstan-property'] as $tagName) { + foreach ($phpDocNode->getPropertyTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - true, - true - ); + $resolved[$propertyName] = new PropertyTag( + $propertyType, + $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 = []; + $originalNameScope = $nameScope; - foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) { + foreach (['@method', '@phan-method', '@psalm-method', '@phpstan-method'] as $tagName) { foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { + $nameScope = $originalNameScope; + $templateTags = []; + + if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) { + foreach ($tagValue->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + $templateType->default !== null + ? $this->typeNodeResolver->resolve($templateType->default, $nameScope) + : null, + TemplateTypeVariance::createInvariant(), + ); + } + + $templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } + $parameters = []; foreach ($tagValue->parameters as $parameterNode) { $parameterName = substr($parameterNode->parameterName, 1); @@ -155,7 +199,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): } $defaultValue = null; if ($parameterNode->defaultValue !== null) { - $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue); + $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue, $nameScope); } $parameters[$parameterName] = new MethodTagParameter( @@ -165,7 +209,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : PassedByReference::createNo(), $parameterNode->isVariadic || $parameterNode->defaultValue !== null, $parameterNode->isVariadic, - $defaultValue + $defaultValue, ); } @@ -174,7 +218,8 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(), $tagValue->isStatic, - $parameters + $parameters, + $templateTags, ); } } @@ -183,16 +228,16 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): } /** - * @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[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -201,7 +246,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) } /** - * @return array + * @return array */ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -210,7 +255,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco foreach (['@implements', '@template-implements', '@phpstan-implements'] as $tagName) { foreach ($phpDocNode->getImplementsTagValues($tagName) as $tagValue) { $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ImplementsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -219,7 +264,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco } /** - * @return array + * @return array */ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -228,7 +273,7 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) { foreach ($phpDocNode->getUsesTagValues($tagName) as $tagValue) { $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new UsesTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -237,9 +282,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 { @@ -248,8 +291,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) { @@ -259,17 +303,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 = ''; @@ -282,10 +330,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->unsetTemplateType($valueNode->name)) : 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; } @@ -294,15 +345,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); @@ -312,7 +361,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): $resolved[$parameterName] = new ParamTag( $parameterType, - $tagValue->isVariadic + $tagValue->isVariadic, ); } } @@ -320,11 +369,82 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ReturnTag + /** + * @return array + */ + public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + if (!method_exists($phpDocNode, 'getParamOutTypeTagValues')) { + return []; + } + + $resolved = []; + + foreach (['@param-out', '@psalm-param-out', '@phpstan-param-out'] as $tagName) { + foreach ($phpDocNode->getParamOutTypeTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + if ($this->shouldSkipType($tagName, $parameterType)) { + continue; + } + + $resolved[$parameterName] = new ParamOutTag( + $parameterType, + ); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): array + { + $parameters = []; + foreach (['@param-immediately-invoked-callable', '@phpstan-param-immediately-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamImmediatelyInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = true; + } + } + foreach (['@param-later-invoked-callable', '@phpstan-param-later-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamLaterInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = false; + } + } + + return $parameters; + } + + /** + * @return array + */ + public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $closureThisTypes = []; + foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) { + foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $closureThisTypes[$parameterName] = new ParamClosureThisTag( + TypeCombinator::intersect( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + new ObjectWithoutClassType(), + ), + ); + } + } + + return $closureThisTypes; + } + + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; - foreach (['@return', '@psalm-return', '@phpstan-return'] as $tagName) { + foreach (['@return', '@phan-return', '@phan-real-return', '@psalm-return', '@phpstan-return'] as $tagName) { foreach ($phpDocNode->getReturnTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); if ($this->shouldSkipType($tagName, $type)) { @@ -337,7 +457,7 @@ public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?ThrowsTag { foreach (['@phpstan-throws', '@throws'] as $tagName) { $types = []; @@ -360,17 +480,67 @@ public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope * @return array */ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { - return array_map(function (MixinTagValueNode $mixinTagValueNode) use ($nameScope): MixinTag { - return new MixinTag( - $this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope) - ); - }, $phpDocNode->getMixinTagValues()); + return array_map(fn (MixinTagValueNode $mixinTagValueNode): MixinTag => new MixinTag( + $this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope), + ), $phpDocNode->getMixinTagValues()); + } + + /** + * @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 resolveSealedTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-inheritors', '@phpstan-sealed'] as $tagName) { + foreach ($phpDocNode->getSealedTagValues($tagName) as $tagValue) { + $resolved[] = new SealedTypeTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + ); + } + } + + return $resolved; } /** @@ -380,7 +550,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop { $resolved = []; - foreach (['@psalm-type', '@phpstan-type'] as $tagName) { + foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; $typeNode = $typeAliasTagValue->type; @@ -410,7 +580,72 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na return $resolved; } - public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + /** + * @return AssertTag[] + */ + public function resolveAssertTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + foreach (['@phpstan', '@psalm', '@phan'] as $prefix) { + $resolved = array_merge( + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert', AssertTag::NULL), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-true', AssertTag::IF_TRUE), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-false', AssertTag::IF_FALSE), + ); + + if (count($resolved) > 0) { + return $resolved; + } + } + + return []; + } + + /** + * @param AssertTag::NULL|AssertTag::IF_TRUE|AssertTag::IF_FALSE $if + * @return AssertTag[] + */ + private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameScope, string $tagName, string $if): array + { + $resolved = []; + + foreach ($phpDocNode->getAssertTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + foreach ($phpDocNode->getAssertPropertyTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, $assertTagValue->property, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + foreach ($phpDocNode->getAssertMethodTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, $assertTagValue->method); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + return $resolved; + } + + public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag + { + if (!method_exists($phpDocNode, 'getSelfOutTypeTagValues')) { + return null; + } + + foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) { + foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) { + $type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope); + return new SelfOutTypeTag($type); + } + } + + return null; + } + + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { $description = (string) $deprecatedTagValue; @@ -427,6 +662,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'); @@ -444,7 +686,7 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool public function resolveIsPure(PhpDocNode $phpDocNode): bool { foreach ($phpDocNode->getTags() as $phpDocTagNode) { - if (in_array($phpDocTagNode->name, ['@pure', '@psalm-pure', '@phpstan-pure'], true)) { + if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) { return true; } } @@ -463,13 +705,70 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool return false; } + public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool + { + foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveIsImmutable(PhpDocNode $phpDocNode): bool + { + foreach (['@immutable', '@phan-immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveHasConsistentConstructor(PhpDocNode $phpDocNode): bool + { + foreach (['@consistent-constructor', '@phpstan-consistent-constructor', '@psalm-consistent-constructor'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveAcceptsNamedArguments(PhpDocNode $phpDocNode): bool + { + return count($phpDocNode->getTagsByName('@no-named-arguments')) === 0; + } + private function shouldSkipType(string $tagName, Type $type): bool { - if (strpos($tagName, '@psalm-') !== 0) { + if (!str_starts_with($tagName, '@psalm-')) { return false; } return $this->unresolvableTypeHelper->containsUnresolvableType($type); } + public function resolveAllowPrivateMutation(PhpDocNode $phpDocNode): bool + { + foreach (['@phpstan-readonly-allow-private-mutation', '@phpstan-allow-private-mutation', '@psalm-readonly-allow-private-mutation', '@psalm-allow-private-mutation'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + } diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index b503e5cd13..30a6e1df0e 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -2,29 +2,25 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; -class PhpDocStringResolver +#[AutowiredService] +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); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $phpDocNode; } diff --git a/src/PhpDoc/ReflectionClassStubFilesExtension.php b/src/PhpDoc/ReflectionClassStubFilesExtension.php new file mode 100644 index 0000000000..f47fe54bba --- /dev/null +++ b/src/PhpDoc/ReflectionClassStubFilesExtension.php @@ -0,0 +1,29 @@ +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..d48519b735 --- /dev/null +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -0,0 +1,29 @@ +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 bba06a95fd..dc43e9af5e 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,22 +3,52 @@ 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\SealedTypeTag; +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; - -/** @api */ -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[] */ @@ -32,84 +62,112 @@ class ResolvedPhpDocBlock 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 $sealedTypeTags = false; /** @var array|false */ - private $typeAliasTags = false; + private array|false $typeAliasTags = false; /** @var array|false */ - private $typeAliasImportTags = false; + private array|false $typeAliasImportTags = false; + + /** @var array|false */ + private array|false $assertTags = false; - /** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ - private $deprecatedTag = 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 $isPure = 'notLoaded'; + 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]; @@ -119,6 +177,23 @@ public static function create( $self->templateTypeMap = $templateTypeMap; $self->templateTags = $templateTags; $self->phpDocNodeResolver = $phpDocNodeResolver; + $self->reflectionProvider = $reflectionProvider; + + return $self; + } + + public function withNameScope(NameScope $nameScope): self + { + $self = new self(); + $self->phpDocNode = $this->phpDocNode; + $self->phpDocNodes = $this->phpDocNodes; + $self->phpDocString = $this->phpDocString; + $self->filename = $this->filename; + $self->nameScope = $nameScope; + $self->templateTypeMap = $this->templateTypeMap; + $self->templateTags = $this->templateTags; + $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; return $self; } @@ -127,7 +202,7 @@ public static function createEmpty(): self { // new property also needs to be added to merge() $self = new self(); - $self->phpDocString = '/** */'; + $self->phpDocString = self::EMPTY_DOC_STRING; $self->phpDocNodes = []; $self->filename = null; $self->templateTypeMap = TemplateTypeMap::createEmpty(); @@ -139,16 +214,30 @@ 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->sealedTypeTags = []; $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; } @@ -156,22 +245,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; @@ -184,34 +279,108 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->implementsTags = $this->getImplementsTags(); $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); - $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks); + $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); + $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); + $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); + $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); + $result->requireExtendsTags = $this->getRequireExtendsTags(); + $result->requireImplementsTags = $this->getRequireImplementsTags(); + $result->sealedTypeTags = $this->getSealedTags(); $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); - $result->deprecatedTag = $this->getDeprecatedTag(); + $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; + $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); - $result->isPure = $this->isPure(); + $result->isPure = self::mergePureTags($this->isPure(), $parents); + $result->isReadOnly = $this->isReadOnly(); + $result->isImmutable = $this->isImmutable(); + $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); + $result->hasConsistentConstructor = $this->hasConsistentConstructor(); + $result->acceptsNamedArguments = $acceptsNamedArguments; return $result; } /** * @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; + } + $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; } - $newParamTags[$parameterNameMapping[$key]] = $paramTag; + + $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(); @@ -223,6 +392,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->templateTypeMap = $this->templateTypeMap; $self->templateTags = $this->templateTags; $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; $self->varTags = $this->varTags; $self->methodTags = $this->methodTags; $self->propertyTags = $this->propertyTags; @@ -230,13 +400,21 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->implementsTags = $this->implementsTags; $self->usesTags = $this->usesTags; $self->paramTags = $newParamTags; - $self->returnTag = $this->returnTag; + $self->paramOutTags = $newParamOutTags; + $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable; + $self->paramClosureThisTags = $newParamClosureThisTags; + $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; + $self->requireImplementsTags = $this->requireImplementsTags; + $self->requireExtendsTags = $this->requireExtendsTags; $self->typeAliasTags = $this->typeAliasTags; $self->typeAliasImportTags = $this->typeAliasImportTags; + $self->assertTags = $assertTags; + $self->selfOutTypeTag = $this->selfOutTypeTag; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; + $self->isNotDeprecated = $this->isNotDeprecated; $self->isInternal = $this->isInternal; $self->isFinal = $this->isFinal; $self->isPure = $this->isPure; @@ -244,6 +422,11 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $self; } + public function hasPhpDocString(): bool + { + return $this->phpDocString !== self::EMPTY_DOC_STRING; + } + public function getPhpDocString(): string { return $this->phpDocString; @@ -273,49 +456,49 @@ public function getNullableNameScope(): ?NameScope } /** - * @return array + * @return array<(string|int), VarTag> */ public function getVarTags(): array { if ($this->varTags === false) { $this->varTags = $this->phpDocNodeResolver->resolveVarTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->varTags; } /** - * @return array + * @return array */ public function getMethodTags(): array { if ($this->methodTags === false) { $this->methodTags = $this->phpDocNodeResolver->resolveMethodTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->methodTags; } /** - * @return array + * @return array */ public function getPropertyTags(): array { if ($this->propertyTags === false) { $this->propertyTags = $this->phpDocNodeResolver->resolvePropertyTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->propertyTags; } /** - * @return array + * @return array */ public function getTemplateTags(): array { @@ -323,78 +506,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->getNameScope() + $this->getNameScope(), ); } return $this->extendsTags; } /** - * @return array + * @return array */ public function getImplementsTags(): array { if ($this->implementsTags === false) { $this->implementsTags = $this->phpDocNodeResolver->resolveImplementsTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->implementsTags; } /** - * @return array + * @return array */ public function getUsesTags(): array { if ($this->usesTags === false) { $this->usesTags = $this->phpDocNodeResolver->resolveUsesTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->usesTags; } /** - * @return array + * @return array */ public function getParamTags(): array { if ($this->paramTags === false) { $this->paramTags = $this->phpDocNodeResolver->resolveParamTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->paramTags; } - public function getReturnTag(): ?\PHPStan\PhpDoc\Tag\ReturnTag + /** + * @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->getNameScope() + $this->getNameScope(), ); } return $this->returnTag; } - public function getThrowsTag(): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function getThrowsTag(): ?ThrowsTag { - if ($this->throwsTag === false) { + if (is_bool($this->throwsTag)) { $this->throwsTag = $this->phpDocNodeResolver->resolveThrowsTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->throwsTag; @@ -408,13 +632,58 @@ public function getMixinTags(): array if ($this->mixinTags === false) { $this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->mixinTags; } + /** + * @return array + */ + public function getRequireExtendsTags(): array + { + if ($this->requireExtendsTags === false) { + $this->requireExtendsTags = $this->phpDocNodeResolver->resolveRequireExtendsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireExtendsTags; + } + + /** + * @return array + */ + public function getRequireImplementsTags(): array + { + if ($this->requireImplementsTags === false) { + $this->requireImplementsTags = $this->phpDocNodeResolver->resolveRequireImplementsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireImplementsTags; + } + + /** + * @return array + */ + public function getSealedTags(): array + { + if ($this->sealedTypeTags === false) { + $this->sealedTypeTags = $this->phpDocNodeResolver->resolveSealedTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->sealedTypeTags; + } + /** * @return array */ @@ -423,7 +692,7 @@ public function getTypeAliasTags(): array if ($this->typeAliasTags === false) { $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } @@ -438,52 +707,94 @@ public function getTypeAliasImportTags(): array if ($this->typeAliasImportTags === false) { $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->typeAliasImportTags; } - public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + /** + * @return array + */ + public function getAssertTags(): array { - if ($this->deprecatedTag === false) { - $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag( + if ($this->assertTags === false) { + $this->assertTags = $this->phpDocNodeResolver->resolveAssertTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } - return $this->deprecatedTag; + + return $this->assertTags; } - public function isDeprecated(): bool + public function getSelfOutTag(): ?SelfOutTypeTag { - if ($this->isDeprecated === null) { - $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated( - $this->phpDocNode + if ($this->selfOutTypeTag === false) { + $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag( + $this->phpDocNode, + $this->getNameScope(), ); } - return $this->isDeprecated; + + return $this->selfOutTypeTag; } - public function isInternal(): bool + public function getDeprecatedTag(): ?DeprecatedTag { - if ($this->isInternal === null) { - $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal( - $this->phpDocNode + if (is_bool($this->deprecatedTag)) { + $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag( + $this->phpDocNode, + $this->getNameScope(), ); } - return $this->isInternal; + return $this->deprecatedTag; + } + + public function isDeprecated(): bool + { + return $this->isDeprecated ??= $this->phpDocNodeResolver->resolveIsDeprecated( + $this->phpDocNode, + ); + } + + /** + * @internal + */ + public function isNotDeprecated(): bool + { + return $this->isNotDeprecated ??= $this->phpDocNodeResolver->resolveIsNotDeprecated( + $this->phpDocNode, + ); + } + + public function isInternal(): bool + { + return $this->isInternal ??= $this->phpDocNodeResolver->resolveIsInternal( + $this->phpDocNode, + ); } public function isFinal(): bool { - if ($this->isFinal === null) { - $this->isFinal = $this->phpDocNodeResolver->resolveIsFinal( - $this->phpDocNode - ); - } - return $this->isFinal; + return $this->isFinal ??= $this->phpDocNodeResolver->resolveIsFinal( + $this->phpDocNode, + ); + } + + public function hasConsistentConstructor(): bool + { + return $this->hasConsistentConstructor ??= $this->phpDocNodeResolver->resolveHasConsistentConstructor( + $this->phpDocNode, + ); + } + + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments ??= $this->phpDocNodeResolver->resolveAcceptsNamedArguments( + $this->phpDocNode, + ); } public function getTemplateTypeMap(): TemplateTypeMap @@ -495,19 +806,19 @@ public function isPure(): ?bool { if ($this->isPure === 'notLoaded') { $pure = $this->phpDocNodeResolver->resolveIsPure( - $this->phpDocNode + $this->phpDocNode, ); if ($pure) { $this->isPure = true; return $this->isPure; - } else { - $impure = $this->phpDocNodeResolver->resolveIsImpure( - $this->phpDocNode - ); - if ($impure) { - $this->isPure = false; - return $this->isPure; - } + } + + $impure = $this->phpDocNodeResolver->resolveIsImpure( + $this->phpDocNode, + ); + if ($impure) { + $this->isPure = false; + return $this->isPure; } $this->isPure = null; @@ -516,6 +827,27 @@ public function isPure(): ?bool return $this->isPure; } + public function isReadOnly(): bool + { + return $this->isReadOnly ??= $this->phpDocNodeResolver->resolveIsReadOnly( + $this->phpDocNode, + ); + } + + public function isImmutable(): bool + { + return $this->isImmutable ??= $this->phpDocNodeResolver->resolveIsImmutable( + $this->phpDocNode, + ); + } + + public function isAllowedPrivateMutation(): bool + { + return $this->isAllowedPrivateMutation ??= $this->phpDocNodeResolver->resolveAllowPrivateMutation( + $this->phpDocNode, + ); + } + /** * @param array $varTags * @param array $parents @@ -542,14 +874,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->toImplicit(), $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; @@ -572,8 +902,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 @@ -585,26 +913,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; } @@ -615,7 +946,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) { @@ -624,13 +955,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; } /** @@ -654,16 +1083,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..cd954ca6b0 --- /dev/null +++ b/src/PhpDoc/SocketSelectStubFilesExtension.php @@ -0,0 +1,25 @@ +phpVersion->getVersionId() >= 80000) { + return [__DIR__ . '/../../stubs/socket_select_php8.stub']; + } + + return [__DIR__ . '/../../stubs/socket_select.stub']; + } + +} diff --git a/src/PhpDoc/StubFilesExtension.php b/src/PhpDoc/StubFilesExtension.php index 237f2a04d3..91267fa583 100644 --- a/src/PhpDoc/StubFilesExtension.php +++ b/src/PhpDoc/StubFilesExtension.php @@ -2,7 +2,22 @@ namespace PHPStan\PhpDoc; -/** @api */ +/** + * This is the extension interface to implement if you want to dynamically + * load stub files based on your logic. As opposed to simply list them in the configuration file. + * + * To register it in the configuration file use the `phpstan.stubFilesExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.stubFilesExtension + * ``` + * + * @api + */ interface StubFilesExtension { diff --git a/src/PhpDoc/StubFilesProvider.php b/src/PhpDoc/StubFilesProvider.php new file mode 100644 index 0000000000..c0e9be78d6 --- /dev/null +++ b/src/PhpDoc/StubFilesProvider.php @@ -0,0 +1,14 @@ + */ private array $classMap = []; @@ -64,21 +60,14 @@ class StubPhpDocProvider /** @var array> */ private array $knownFunctionParameterNames = []; - /** - * @param \PHPStan\Parser\Parser $parser - * @param string[] $stubFiles - */ public function __construct( - Parser $parser, - FileTypeMapper $fileTypeMapper, - Container $container, - array $stubFiles + #[AutowiredParameter(ref: '@stubParser')] + private Parser $parser, + #[AutowiredParameter(ref: '@stubFileTypeMapper')] + private FileTypeMapper $fileTypeMapper, + private StubFilesProvider $stubFilesProvider, ) { - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->container = $container; - $this->stubFiles = $stubFiles; } public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock @@ -98,7 +87,7 @@ public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock $className, null, null, - $docComment + $docComment, ); return $this->classMap[$className]; @@ -124,7 +113,7 @@ public function findPropertyPhpDoc(string $className, string $propertyName): ?Re $className, null, null, - $docComment + $docComment, ); return $this->propertyMap[$className][$propertyName]; @@ -150,7 +139,7 @@ public function findClassConstantPhpDoc(string $className, string $constantName) $className, null, null, - $docComment + $docComment, ); return $this->constantMap[$className][$constantName]; @@ -160,12 +149,14 @@ public function findClassConstantPhpDoc(string $className, string $constantName) } /** - * @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; @@ -182,11 +173,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]; @@ -205,10 +202,8 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p } /** - * @param string $functionName * @param array $positionalParameterNames - * @return ResolvedPhpDocBlock|null - * @throws \PHPStan\ShouldNotHappenException + * @throws ShouldNotHappenException */ public function findFunctionPhpDoc(string $functionName, array $positionalParameterNames): ?ResolvedPhpDocBlock { @@ -227,11 +222,11 @@ public function findFunctionPhpDoc(string $functionName, array $positionalParame null, null, $functionName, - $docComment + $docComment, ); if (!isset($this->knownFunctionParameterNames[$functionName])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionParameterNames = $this->knownFunctionParameterNames[$functionName]; @@ -276,7 +271,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; @@ -285,7 +280,7 @@ private function initializeKnownElements(): void $this->initializing = true; try { - foreach ($this->getStubFiles() as $stubFile) { + foreach ($this->stubFilesProvider->getStubFiles() as $stubFile) { $nodes = $this->parser->parseFile($stubFile); foreach ($nodes as $node) { $this->initializeKnownElementNode($stubFile, $node); @@ -297,23 +292,6 @@ private function initializeKnownElements(): void } } - /** - * @return string[] - */ - private function getStubFiles(): array - { - $stubFiles = $this->stubFiles; - $extensions = $this->container->getServicesByTag(StubFilesExtension::EXTENSION_TAG); - foreach ($extensions as $extension) { - $extensionFiles = $extension->getFiles(); - foreach ($extensionFiles as $extensionFile) { - $stubFiles[] = $extensionFile; - } - } - - return $stubFiles; - } - private function initializeKnownElementNode(string $stubFile, Node $node): void { if ($node instanceof Node\Stmt\Namespace_) { @@ -332,7 +310,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void } $this->knownFunctionParameterNames[$functionName] = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; @@ -342,7 +320,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void return; } - if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_) { + if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_ && !$node instanceof Node\Stmt\Enum_) { return; } @@ -393,7 +371,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 4b461f7537..6eca6b0bab 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -2,58 +2,50 @@ namespace PHPStan\PhpDoc; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\DependencyInjection\Container; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; +use function dirname; -class StubSourceLocatorFactory +final class StubSourceLocatorFactory { - private \PhpParser\Parser $php8Parser; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; - - private \PHPStan\DependencyInjection\Container $container; - - /** @var string[] */ - private array $stubFiles; - - /** - * @param string[] $stubFiles - */ public function __construct( - \PhpParser\Parser $php8Parser, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - Container $container, - array $stubFiles + private Parser $php8Parser, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private StubFilesProvider $stubFilesProvider, ) { - $this->php8Parser = $php8Parser; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->container = $container; - $this->stubFiles = $stubFiles; } public function create(): SourceLocator { $locators = []; - $astPhp8Locator = new Locator($this->php8Parser, 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[] = $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 3e7ad505af..76830f34e2 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -4,26 +4,52 @@ 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\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\DerivativeContainerFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Classes\DuplicateClassDeclarationRule; +use PHPStan\Rules\Classes\DuplicateDeclarationRule; use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule; use PHPStan\Rules\Classes\ExistingClassInClassExtendsRule; use PHPStan\Rules\Classes\ExistingClassInTraitUseRule; +use PHPStan\Rules\Classes\LocalTypeAliasesCheck; +use PHPStan\Rules\Classes\LocalTypeAliasesRule; +use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule; +use PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule; +use PHPStan\Rules\Classes\MethodTagCheck; +use PHPStan\Rules\Classes\MethodTagRule; +use PHPStan\Rules\Classes\MethodTagTraitRule; +use PHPStan\Rules\Classes\MethodTagTraitUseRule; +use PHPStan\Rules\Classes\MixinCheck; +use PHPStan\Rules\Classes\MixinRule; +use PHPStan\Rules\Classes\MixinTraitRule; +use PHPStan\Rules\Classes\MixinTraitUseRule; +use PHPStan\Rules\Classes\PropertyTagCheck; +use PHPStan\Rules\Classes\PropertyTagRule; +use PHPStan\Rules\Classes\PropertyTagTraitRule; +use PHPStan\Rules\Classes\PropertyTagTraitUseRule; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Functions\DuplicateFunctionDeclarationRule; use PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule; use PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule; use PHPStan\Rules\Generics\ClassAncestorsRule; use PHPStan\Rules\Generics\ClassTemplateTypeRule; use PHPStan\Rules\Generics\CrossCheckInterfacesHelper; +use PHPStan\Rules\Generics\EnumAncestorsRule; +use PHPStan\Rules\Generics\EnumTemplateTypeRule; use PHPStan\Rules\Generics\FunctionSignatureVarianceRule; use PHPStan\Rules\Generics\FunctionTemplateTypeRule; use PHPStan\Rules\Generics\GenericAncestorsCheck; @@ -31,42 +57,65 @@ 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\MethodPrototypeFinder; 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 +#[AutowiredService] +final class StubValidator { - private \PHPStan\DependencyInjection\DerivativeContainerFactory $derivativeContainerFactory; - public function __construct( - DerivativeContainerFactory $derivativeContainerFactory + private DerivativeContainerFactory $derivativeContainerFactory, ) { - $this->derivativeContainerFactory = $derivativeContainerFactory; } /** * @param string[] $stubFiles - * @return \PHPStan\Analyser\Error[] + * @return list */ public function validate(array $stubFiles, bool $debug): array { @@ -74,54 +123,65 @@ public function validate(array $stubFiles, bool $debug): array return []; } - $originalBroker = Broker::getInstance(); $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - $container = $this->derivativeContainerFactory->create([ - __DIR__ . '/../../conf/config.stubValidator.neon', - ]); + $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); + + try { + $container = $this->derivativeContainerFactory->create([ + __DIR__ . '/../../conf/config.stubValidator.neon', + ]); - $ruleRegistry = $this->getRuleRegistry($container); + $ruleRegistry = $this->getRuleRegistry($container); + $collectorRegistry = $this->getCollectorRegistry($container); - /** @var FileAnalyser $fileAnalyser */ - $fileAnalyser = $container->getByType(FileAnalyser::class); + $fileAnalyser = $container->getByType(FileAnalyser::class); - /** @var NodeScopeResolver $nodeScopeResolver */ - $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); - $nodeScopeResolver->setAnalysedFiles($stubFiles); + $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); + $nodeScopeResolver->setAnalysedFiles($stubFiles); - $analysedFiles = array_fill_keys($stubFiles, true); + $pathRoutingParser = $container->getService('pathRoutingParser'); + $pathRoutingParser->setAnalysedFiles($stubFiles); - $errors = []; - foreach ($stubFiles as $stubFile) { - try { - $tmpErrors = $fileAnalyser->analyseFile( - $stubFile, - $analysedFiles, - $ruleRegistry, - static function (): void { + $analysedFiles = array_fill_keys($stubFiles, true); + + $errors = []; + foreach ($stubFiles as $stubFile) { + try { + $tmpErrors = $fileAnalyser->analyseFile( + $stubFile, + $analysedFiles, + $ruleRegistry, + $collectorRegistry, + static function (): void { + }, + )->getErrors(); + foreach ($tmpErrors as $tmpError) { + $errors[] = $tmpError->withoutTip()->doNotIgnore(); + } + } catch (Throwable $e) { + if ($debug) { + throw $e; } - )->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); + $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); + $errors[] = (new Error($internalErrorMessage, $stubFile, canBeIgnored: $e)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + } } + } finally { + ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); + ObjectType::resetCaches(); } - Broker::registerInstance($originalBroker); - ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); - 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); @@ -129,44 +189,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); $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 OverridingMethodRule($container->getByType(PhpVersion::class), new MethodSignatureRule(true, true), true), + new ExistingClassesInPropertiesRule($reflectionProvider, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false, $discoveringSymbolsTip), + new OverridingMethodRule( + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, true, true), + true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), + $container->getParameter('checkMissingOverrideMethodAttribute'), + ), + new DuplicateDeclarationRule(), + new LocalTypeAliasesRule($localTypeAliasesCheck), + new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider), + new LocalTypeTraitUseAliasesRule($localTypeAliasesCheck), // level 2 - new ClassAncestorsRule($fileTypeMapper, $genericAncestorsCheck, $crossCheckInterfacesHelper), + new ClassAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), new ClassTemplateTypeRule($templateTypeCheck), new FunctionTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new FunctionSignatureVarianceRule($varianceCheck), - new InterfaceAncestorsRule($fileTypeMapper, $genericAncestorsCheck, $crossCheckInterfacesHelper), - 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, - $unresolvableTypeHelper - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, new IncompatiblePhpDocTypeCheck($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper)), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::class) + $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), @@ -174,9 +281,19 @@ private function getRuleRegistry(Container $container): Registry new MissingMethodParameterTypehintRule($missingTypehintCheck), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), + new MissingMethodSelfOutTypeRule($missingTypehintCheck), + + // duplicate stubs + new DuplicateClassDeclarationRule($reflector, $relativePathHelper), + new DuplicateFunctionDeclarationRule($reflector, $relativePathHelper), ]; - return new Registry($rules); + return new DirectRuleRegistry($rules); + } + + private function getCollectorRegistry(Container $container): CollectorRegistry + { + return new CollectorRegistry([]); } } diff --git a/src/PhpDoc/Tag/AssertTag.php b/src/PhpDoc/Tag/AssertTag.php new file mode 100644 index 0000000000..301b5b4876 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTag.php @@ -0,0 +1,96 @@ +if; + } + + public function getType(): Type + { + return $this->type; + } + + public function getOriginalType(): Type + { + return $this->originalType ??= $this->type; + } + + public function getParameter(): AssertTagParameter + { + return $this->parameter; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isEquality(): bool + { + return $this->equality; + } + + /** + * @return static + */ + public function withType(Type $type): TypedTag + { + $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function withParameter(AssertTagParameter $parameter): self + { + $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function negate(): self + { + if ($this->isEquality()) { + throw new ShouldNotHappenException(); + } + + $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->if, $this->type, $this->parameter, $this->negated, $this->equality, false); + } + +} diff --git a/src/PhpDoc/Tag/AssertTagParameter.php b/src/PhpDoc/Tag/AssertTagParameter.php new file mode 100644 index 0000000000..a3c3250326 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTagParameter.php @@ -0,0 +1,59 @@ +parameterName; + } + + public function changeParameterName(string $parameterName): self + { + return new self( + $parameterName, + $this->property, + $this->method, + ); + } + + public function describe(): string + { + if ($this->property !== null) { + return sprintf('%s->%s', $this->parameterName, $this->property); + } + + if ($this->method !== null) { + return sprintf('%s->%s()', $this->parameterName, $this->method); + } + + return $this->parameterName; + } + + public function getExpr(Expr $parameter): Expr + { + if ($this->property !== null) { + return new Expr\PropertyFetch($parameter, $this->property); + } + + if ($this->method !== null) { + return new Expr\MethodCall($parameter, $this->method); + } + + return $parameter; + } + +} diff --git a/src/PhpDoc/Tag/DeprecatedTag.php b/src/PhpDoc/Tag/DeprecatedTag.php index 1e6309658f..9bc036e1d8 100644 --- a/src/PhpDoc/Tag/DeprecatedTag.php +++ b/src/PhpDoc/Tag/DeprecatedTag.php @@ -2,15 +2,14 @@ namespace PHPStan\PhpDoc\Tag; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/ExtendsTag.php b/src/PhpDoc/Tag/ExtendsTag.php index 8e7330c273..72cb97f7cf 100644 --- a/src/PhpDoc/Tag/ExtendsTag.php +++ b/src/PhpDoc/Tag/ExtendsTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/ImplementsTag.php b/src/PhpDoc/Tag/ImplementsTag.php index 17fcc7e310..556959b68d 100644 --- a/src/PhpDoc/Tag/ImplementsTag.php +++ b/src/PhpDoc/Tag/ImplementsTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index 2f8ba3bea4..43bda4cf97 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -4,31 +4,23 @@ use PHPStan\Type\Type; -/** @api */ -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 @@ -42,11 +34,19 @@ public function isStatic(): bool } /** - * @return array + * @return array */ public function getParameters(): array { return $this->parameters; } + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + } diff --git a/src/PhpDoc/Tag/MethodTagParameter.php b/src/PhpDoc/Tag/MethodTagParameter.php index 6f4043682e..3e4c817bf8 100644 --- a/src/PhpDoc/Tag/MethodTagParameter.php +++ b/src/PhpDoc/Tag/MethodTagParameter.php @@ -5,33 +5,20 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/MixinTag.php b/src/PhpDoc/Tag/MixinTag.php index 847af7b1d8..c115c2cacb 100644 --- a/src/PhpDoc/Tag/MixinTag.php +++ b/src/PhpDoc/Tag/MixinTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 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 04d8fc76b0..498dd64ce7 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -4,18 +4,17 @@ use PHPStan\Type\Type; -/** @api */ -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 @@ -28,11 +27,7 @@ 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); } diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index 4b624dccf3..16090c44b0 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -4,40 +4,43 @@ use PHPStan\Type\Type; -/** @api */ -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->readableType; + } + + public function getWritableType(): ?Type { - return $this->type; + return $this->writableType; } + /** + * @phpstan-assert-if-true !null $this->getReadableType() + */ public function isReadable(): bool { - return $this->readable; + return $this->readableType !== null; } + /** + * @phpstan-assert-if-true !null $this->getWritableType() + */ public function isWritable(): bool { - return $this->writable; + return $this->writableType !== null; } } diff --git a/src/PhpDoc/Tag/RequireExtendsTag.php b/src/PhpDoc/Tag/RequireExtendsTag.php new file mode 100644 index 0000000000..97bf685468 --- /dev/null +++ b/src/PhpDoc/Tag/RequireExtendsTag.php @@ -0,0 +1,22 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/RequireImplementsTag.php b/src/PhpDoc/Tag/RequireImplementsTag.php new file mode 100644 index 0000000000..aafd560260 --- /dev/null +++ b/src/PhpDoc/Tag/RequireImplementsTag.php @@ -0,0 +1,22 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/ReturnTag.php b/src/PhpDoc/Tag/ReturnTag.php index aa75fcc5b2..b501dd67e1 100644 --- a/src/PhpDoc/Tag/ReturnTag.php +++ b/src/PhpDoc/Tag/ReturnTag.php @@ -4,18 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 @@ -28,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); } diff --git a/src/PhpDoc/Tag/SealedTypeTag.php b/src/PhpDoc/Tag/SealedTypeTag.php new file mode 100644 index 0000000000..51d73aacf7 --- /dev/null +++ b/src/PhpDoc/Tag/SealedTypeTag.php @@ -0,0 +1,27 @@ +type; + } + + public function withType(Type $type): self + { + return new self($type); + } + +} 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 b936b8c640..bafa555833 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -5,23 +5,22 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; -/** @api */ -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; @@ -32,6 +31,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function getVariance(): TemplateTypeVariance { return $this->variance; diff --git a/src/PhpDoc/Tag/ThrowsTag.php b/src/PhpDoc/Tag/ThrowsTag.php index 1fbe1e2dc0..1c1e30b897 100644 --- a/src/PhpDoc/Tag/ThrowsTag.php +++ b/src/PhpDoc/Tag/ThrowsTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php index 8edd46080f..ab074d6775 100644 --- a/src/PhpDoc/Tag/TypeAliasImportTag.php +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -6,17 +6,8 @@ final class TypeAliasImportTag { - private string $importedAlias; - - private string $importedFrom; - - private ?string $importedAs; - - public function __construct(string $importedAlias, string $importedFrom, ?string $importedAs) + public function __construct(private string $importedAlias, private string $importedFrom, private ?string $importedAs) { - $this->importedAlias = $importedAlias; - $this->importedFrom = $importedFrom; - $this->importedAs = $importedAs; } public function getImportedAlias(): string diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index 8782dbd907..d5cd10e5d6 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -4,26 +4,20 @@ use PHPStan\Analyser\NameScope; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\TypeAlias; -/** @api */ -class TypeAliasTag +/** + * @api + */ +final class TypeAliasTag { - private string $aliasName; - - private TypeNode $typeNode; - - private NameScope $nameScope; - public function __construct( - string $aliasName, - TypeNode $typeNode, - NameScope $nameScope + private string $aliasName, + private TypeNode $typeNode, + private NameScope $nameScope, ) { - $this->aliasName = $aliasName; - $this->typeNode = $typeNode; - $this->nameScope = $nameScope; } public function getAliasName(): string @@ -31,11 +25,11 @@ public function getAliasName(): string return $this->aliasName; } - public function getTypeAlias(): \PHPStan\Type\TypeAlias + public function getTypeAlias(): TypeAlias { - return new \PHPStan\Type\TypeAlias( + return new TypeAlias( $this->typeNode, - $this->nameScope + $this->nameScope, ); } diff --git a/src/PhpDoc/Tag/TypedTag.php b/src/PhpDoc/Tag/TypedTag.php index 5d6f9c6bee..0be9218dce 100644 --- a/src/PhpDoc/Tag/TypedTag.php +++ b/src/PhpDoc/Tag/TypedTag.php @@ -11,7 +11,6 @@ interface TypedTag public function getType(): Type; /** - * @param Type $type * @return static */ public function withType(Type $type): self; diff --git a/src/PhpDoc/Tag/UsesTag.php b/src/PhpDoc/Tag/UsesTag.php index 950f58cba6..1679997ed3 100644 --- a/src/PhpDoc/Tag/UsesTag.php +++ b/src/PhpDoc/Tag/UsesTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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 diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index 6c9a04d2ee..9d594fd75c 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -4,15 +4,14 @@ use PHPStan\Type\Type; -/** @api */ -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, private bool $isExplicit) { - $this->type = $type; } public function getType(): Type @@ -20,13 +19,19 @@ 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, $this->isExplicit); + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self { - return new self($type); + return new self($this->type, false); } } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 39ac549568..ea29d7c088 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -2,9 +2,16 @@ 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\DependencyInjection\AutowiredService; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -13,24 +20,39 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; 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; @@ -38,50 +60,87 @@ 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; + +#[AutowiredService] +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 */ @@ -109,6 +168,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); @@ -120,8 +185,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(); @@ -131,7 +202,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco { switch (strtolower($typeNode->name)) { case 'int': + return new IntegerType(); + case 'integer': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + return new IntegerType(); case 'positive-int': @@ -140,9 +219,27 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'negative-int': return IntegerRangeType::fromInterval(null, -1); + case 'non-positive-int': + return IntegerRangeType::fromInterval(null, 0); + + case 'non-negative-int': + return IntegerRangeType::fromInterval(0, null); + + case 'non-zero-int': + return new UnionType([ + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]); + case 'string': return new StringType(); + case 'lowercase-string': + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + + case 'uppercase-string': + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); @@ -151,6 +248,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'trait-string': return new ClassStringType(); + case 'enum-string': + return new GenericClassStringType(new ObjectType('UnitEnum')); + case 'callable-string': return new IntersectionType([new StringType(), new CallableType()]); @@ -166,6 +266,18 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]); + case 'empty-scalar': + return TypeCombinator::intersect( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + + case 'non-empty-scalar': + return TypeCombinator::remove( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + case 'number': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -203,6 +315,34 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryNonEmptyStringType(), ]); + case 'non-empty-lowercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLowercaseStringType(), + ]); + + case 'non-empty-uppercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryUppercaseStringType(), + ]); + + case 'truthy-string': + case 'non-falsy-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + + case 'non-empty-literal-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLiteralStringType(), + ]); + case 'bool': return new BooleanType(); @@ -243,7 +383,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'non-empty-array': return TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType() + new NonEmptyArrayType(), ); case 'iterable': @@ -252,6 +392,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'callable': return new CallableType(); + case 'pure-callable': + return new CallableType(isPure: TrinaryLogic::createYes()); + + case 'pure-closure': + return ClosureType::createPure(); + case 'resource': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -261,37 +407,61 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ResourceType(); + case 'open-resource': + case 'closed-resource': + return new ResourceType(); + case 'mixed': return new MixedType(true); + case 'non-empty-mixed': + return new MixedType(true, StaticTypeFactory::falsey()); + case 'void': return new VoidType(); case 'object': return new ObjectWithoutClassType(); + case 'callable-object': + return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]); + + case 'callable-array': + return new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]); + case 'never': + case 'noreturn': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); if ($type !== null) { return $type; } - return new NeverType(true); + return new NonAcceptingNeverType(); case 'never-return': case 'never-returns': case 'no-return': - case 'noreturn': - return new NeverType(true); + return new NonAcceptingNeverType(); case 'list': - return new ArrayType(new IntegerType(), new MixedType()); + return TypeCombinator::intersect(new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()); case 'non-empty-list': return TypeCombinator::intersect( - new ArrayType(new IntegerType(), new MixedType()), - new NonEmptyArrayType() + 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) { @@ -332,13 +502,38 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } $stringName = $nameScope->resolveStringName($typeNode->name); - if (strpos($stringName, '-') !== false && strpos($stringName, 'OCI-') !== 0) { + if (str_contains($stringName, '-') && !str_starts_with($stringName, 'OCI-')) { return new ErrorType(); } + if ($this->mightBeConstant($typeNode->name) && !$this->getReflectionProvider()->hasClass($stringName)) { + $constType = $this->tryResolveConstant($typeNode->name, $nameScope); + if ($constType !== null) { + return $constType; + } + } + return new ObjectType($stringName); } + private function mightBeConstant(string $name): bool + { + return preg_match('((?:^|\\\\)[A-Z_][A-Z0-9_]*$)', $name) > 0; + } + + private function tryResolveConstant(string $name, NameScope $nameScope): ?Type + { + foreach ($nameScope->resolveConstantNames($name) as $constName) { + $nameNode = new Name\FullyQualified(explode('\\', $constName)); + $constType = $this->constantResolver->resolveConstant($nameNode, null); + if ($constType !== null) { + return $constType; + } + } + + return null; + } + private function tryResolvePseudoTypeClassType(IdentifierTypeNode $typeNode, NameScope $nameScope): ?Type { if ($nameScope->hasUseAlias($typeNode->name)) { @@ -372,7 +567,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 @@ -399,10 +594,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 { @@ -426,6 +623,28 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam return TypeCombinator::intersect(...$types); } + private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type + { + return new ConditionalType( + $this->resolve($typeNode->subjectType, $nameScope), + $this->resolve($typeNode->targetType, $nameScope), + $this->resolve($typeNode->if, $nameScope), + $this->resolve($typeNode->else, $nameScope), + $typeNode->negated, + ); + } + + private function resolveConditionalTypeForParameterNode(ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type + { + return new ConditionalTypeForParameter( + $typeNode->parameterName, + $this->resolve($typeNode->targetType, $nameScope), + $this->resolve($typeNode->if, $nameScope), + $this->resolve($typeNode->else, $nameScope), + $typeNode->negated, + ); + } + private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type { $itemType = $this->resolve($typeNode->type, $nameScope); @@ -436,16 +655,41 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na { $mainTypeName = strtolower($typeNode->type->name); $genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope); + $variances = array_map( + static function (string $variance): TemplateTypeVariance { + switch ($variance) { + case GenericTypeNode::VARIANCE_INVARIANT: + return TemplateTypeVariance::createInvariant(); + case GenericTypeNode::VARIANCE_COVARIANT: + return TemplateTypeVariance::createCovariant(); + case GenericTypeNode::VARIANCE_CONTRAVARIANT: + return TemplateTypeVariance::createContravariant(); + case GenericTypeNode::VARIANCE_BIVARIANT: + return TemplateTypeVariance::createBivariant(); + } + }, + $typeNode->variances, + ); - if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') { + if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $keyType = TypeCombinator::intersect($genericTypes[0], new UnionType([ + $keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([ new IntegerType(), new StringType(), - ])); - $arrayType = new ArrayType($keyType, $genericTypes[1]); + ]))->toArrayKey(); + $finiteTypes = $keyType->getFiniteTypes(); + if ( + count($finiteTypes) === 1 + && ($finiteTypes[0] instanceof ConstantStringType || $finiteTypes[0] instanceof ConstantIntegerType) + ) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $arrayBuilder->setOffsetValueType($finiteTypes[0], $genericTypes[1], true); + $arrayType = $arrayBuilder->getArray(); + } else { + $arrayType = new ArrayType($keyType, $genericTypes[1]); + } } else { return new ErrorType(); } @@ -455,9 +699,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } return $arrayType; - } elseif ($mainTypeName === 'list' || $mainTypeName === 'non-empty-list') { + } elseif (in_array($mainTypeName, ['list', 'non-empty-list'], true)) { if (count($genericTypes) === 1) { // list - $listType = new ArrayType(new IntegerType(), $genericTypes[0]); + $listType = TypeCombinator::intersect(new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $genericTypes[0]), new AccessoryArrayListType()); if ($mainTypeName === 'non-empty-list') { return TypeCombinator::intersect($listType, new NonEmptyArrayType()); } @@ -478,11 +722,18 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } elseif (in_array($mainTypeName, ['class-string', 'interface-string'], true)) { if (count($genericTypes) === 1) { $genericType = $genericTypes[0]; - if ((new ObjectWithoutClassType())->isSuperTypeOf($genericType)->yes() || $genericType instanceof MixedType) { + if ($genericType->isObject()->yes() || $genericType instanceof MixedType) { return new GenericClassStringType($genericType); } } + return new ErrorType(); + } elseif ($mainTypeName === 'enum-string') { + if (count($genericTypes) === 1) { + $genericType = $genericTypes[0]; + return new GenericClassStringType(TypeCombinator::intersect($genericType, new ObjectType('UnitEnum'))); + } + return new ErrorType(); } elseif ($mainTypeName === 'int') { if (count($genericTypes) === 2) { // int, int<1, 3> @@ -505,89 +756,203 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na 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, variances: $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], + ], variances: [ + TemplateTypeVariance::createInvariant(), + $variances[0], ]); } if (count($genericTypes) === 2) { - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], + ], variances: [ + $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, + ], variances: [ + 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, + ], variances: [ + $variances[0], + $variances[1], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $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, variances: $variances); } return new ErrorType(); @@ -595,36 +960,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 (strpos($parameterName, '$') === 0) { + if (str_starts_with($parameterName, '$')) { $parameterName = substr($parameterName, 1); } + return new NativeParameterReflection( $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, templateTags: $templateTags, isPure: $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, templateTags: $templateTags, impurePoints: [ + new SimpleImpurePoint( + 'functionCall', + 'call to a Closure', + false, + ), + ]); + } elseif ($mainType instanceof ClosureType) { + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments(), mustUseReturnValue: $mainType->mustUseReturnValue()); + if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return $closure; } return new ErrorType(); @@ -633,29 +1046,122 @@ 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; - if ($itemNode->keyName instanceof ConstExprIntegerNode) { - $offsetType = new ConstantIntegerType((int) $itemNode->keyName->value); - } elseif ($itemNode->keyName instanceof IdentifierTypeNode) { - $offsetType = new ConstantStringType($itemNode->keyName->name); + $offsetType = $this->resolveArrayShapeOffsetType($itemNode, $nameScope); + $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); + } + + $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 resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameScope $nameScope): ?Type + { + if ($itemNode->keyName instanceof ConstExprIntegerNode) { + return new ConstantIntegerType((int) $itemNode->keyName->value); + } elseif ($itemNode->keyName instanceof IdentifierTypeNode) { + return new ConstantStringType($itemNode->keyName->name); + } elseif ($itemNode->keyName instanceof ConstExprStringNode) { + return new ConstantStringType($itemNode->keyName->value); + } elseif ($itemNode->keyName instanceof ConstFetchNode) { + $constExpr = $itemNode->keyName; + if ($constExpr->className === '') { + throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode + } + + if ($nameScope->getClassName() !== null) { + switch (strtolower($constExpr->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($constExpr->className); + } + + if (!$this->getReflectionProvider()->hasClass($className)) { + return new ErrorType(); + } + $classReflection = $this->getReflectionProvider()->getClass($className); + + $constantName = $constExpr->name; + if (!$classReflection->hasConstant($constantName)) { + return new ErrorType(); + } + + $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)); + } elseif ($itemNode->keyName !== null) { + throw new ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); + } + + return null; + } + + 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) { - $offsetType = new ConstantStringType($itemNode->keyName->value); - } elseif ($itemNode->keyName !== null) { - throw new \PHPStan\ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); + $propertyName = $itemNode->keyName->value; } - $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); + + if ($itemNode->optional) { + $optionalProperties[] = $propertyName; + } + + $properties[$propertyName] = $this->resolve($itemNode->valueType, $nameScope); } - return $builder->getArray(); + 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 ( @@ -663,12 +1169,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) { @@ -688,6 +1194,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc $className = $classReflection->getParentClass()->getName(); } + break; } } @@ -706,12 +1213,28 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc // convert * into .*? and escape everything else so the constants can be matched against the pattern $pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D'; $constantTypes = []; - foreach ($classReflection->getNativeReflection()->getConstants() as $classConstantName => $constantValue) { + foreach ($classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) { + $classConstantName = $reflectionConstant->getName(); if (Strings::match($classConstantName, $pattern) === null) { continue; } - $constantTypes[] = ConstantTypeHelper::getTypeFromValue($constantValue); + if ($classReflection->isEnum() && $classReflection->hasEnumCase($classConstantName)) { + $constantTypes[] = new EnumCaseObjectType($classReflection->getName(), $classConstantName); + continue; + } + + $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) { @@ -725,29 +1248,86 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } - return ConstantTypeHelper::getTypeFromValue($classReflection->getConstant($constantName)->getValue()); + 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); + } + + if (count($values) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + 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 { @@ -761,12 +1341,12 @@ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array private function getReflectionProvider(): ReflectionProvider { - return $this->container->getByType(ReflectionProvider::class); + return $this->reflectionProviderProvider->getReflectionProvider(); } private function getTypeAliasResolver(): TypeAliasResolver { - return $this->container->getByType(TypeAliasResolver::class); + return $this->typeAliasResolverProvider->getTypeAliasResolver(); } } diff --git a/src/PhpDoc/TypeNodeResolverExtension.php b/src/PhpDoc/TypeNodeResolverExtension.php index 36508c7834..de004ecbba 100644 --- a/src/PhpDoc/TypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeNodeResolverExtension.php @@ -6,7 +6,23 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Type; -/** @api */ +/** + * This is the interface type node resolver extensions implement for custom PHPDoc types. + * + * To register it in the configuration file use the `phpstan.phpDoc.typeNodeResolverExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.phpDoc.typeNodeResolverExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/custom-phpdoc-types + * + * @api + */ interface TypeNodeResolverExtension { diff --git a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php 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 91e8ec4df0..9aa0b151bc 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -3,32 +3,26 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\Type\Type; -class TypeStringResolver +#[AutowiredService] +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); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $this->typeNodeResolver->resolve($typeNode, $nameScope ?? new NameScope(null, [])); } diff --git a/src/Process/CpuCoreCounter.php b/src/Process/CpuCoreCounter.php index 2bc1b2b0a2..a0558bcbeb 100644 --- a/src/Process/CpuCoreCounter.php +++ b/src/Process/CpuCoreCounter.php @@ -2,7 +2,12 @@ namespace PHPStan\Process; -class CpuCoreCounter +use Fidry\CpuCoreCounter\CpuCoreCounter as FidryCpuCoreCounter; +use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound; +use PHPStan\DependencyInjection\AutowiredService; + +#[AutowiredService] +final class CpuCoreCounter { private ?int $count = null; @@ -13,42 +18,13 @@ public function getNumberOfCpuCores(): int return $this->count; } - if (!function_exists('proc_open')) { - return $this->count = 1; - } - - // from brianium/paratest - if (@is_file('/proc/cpuinfo')) { - // Linux (and potentially Windows with linux sub systems) - $cpuinfo = @file_get_contents('/proc/cpuinfo'); - if ($cpuinfo !== false) { - preg_match_all('/^processor/m', $cpuinfo, $matches); - return $this->count = count($matches[0]); - } - } - - if (\DIRECTORY_SEPARATOR === '\\') { - // Windows - $process = @popen('wmic cpu get NumberOfLogicalProcessors', 'rb'); - if (is_resource($process)) { - fgets($process); - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; - } - } - - $process = @\popen('sysctl -n hw.ncpu', 'rb'); - if (is_resource($process)) { - // *nix (Linux, BSD and Mac) - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; + try { + $this->count = (new FidryCpuCoreCounter())->getCount(); + } catch (NumberOfCpuCoreNotFound) { + $this->count = 1; } - return $this->count = 2; + return $this->count; } } diff --git a/src/Process/ProcessCanceledException.php b/src/Process/ProcessCanceledException.php index fb1e5775d8..ae42c75d3b 100644 --- a/src/Process/ProcessCanceledException.php +++ b/src/Process/ProcessCanceledException.php @@ -2,7 +2,9 @@ namespace PHPStan\Process; -class ProcessCanceledException extends \Exception +use Exception; + +final class ProcessCanceledException extends Exception { } diff --git a/src/Process/ProcessCrashedException.php b/src/Process/ProcessCrashedException.php index 8ecd33c4b7..fb75a7d94d 100644 --- a/src/Process/ProcessCrashedException.php +++ b/src/Process/ProcessCrashedException.php @@ -2,7 +2,9 @@ namespace PHPStan\Process; -class ProcessCrashedException extends \Exception +use Exception; + +final class ProcessCrashedException extends Exception { } diff --git a/src/Process/ProcessHelper.php b/src/Process/ProcessHelper.php index c3d6a7d05e..591e916fee 100644 --- a/src/Process/ProcessHelper.php +++ b/src/Process/ProcessHelper.php @@ -4,24 +4,27 @@ use PHPStan\Command\AnalyseCommand; use Symfony\Component\Console\Input\InputInterface; +use function array_merge; +use function escapeshellarg; +use function implode; +use function ini_get; +use function is_bool; +use function php_ini_loaded_file; +use function sprintf; +use const PHP_BINARY; -class ProcessHelper +final class ProcessHelper { /** - * @param string $mainScript - * @param string $commandName - * @param string|null $projectConfigFile * @param string[] $additionalItems - * @param InputInterface $input - * @return string */ public static function getWorkerCommand( string $mainScript, string $commandName, ?string $projectConfigFile, array $additionalItems, - InputInterface $input + InputInterface $input, ): string { $phpIni = php_ini_loaded_file(); @@ -46,11 +49,11 @@ public static function getWorkerCommand( } $options = [ - 'paths-file', AnalyseCommand::OPTION_LEVEL, 'autoload-file', 'memory-limit', 'xdebug', + 'verbose', ]; foreach ($options as $optionName) { /** @var bool|string|null $optionValue */ diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 8441d82427..31c9f0b761 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -2,59 +2,48 @@ namespace PHPStan\Process; -use PHPStan\Process\Runnable\Runnable; +use PHPStan\ShouldNotHappenException; use React\ChildProcess\Process; use React\EventLoop\LoopInterface; -use React\Promise\CancellablePromiseInterface; use React\Promise\Deferred; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; +use function fclose; +use function rewind; +use function stream_get_contents; +use function tmpfile; -class ProcessPromise implements Runnable +final class ProcessPromise { - /** @var LoopInterface */ - private $loop; - - /** @var string */ - private $name; - - /** @var string */ - private $command; - + /** @var Deferred */ private Deferred $deferred; private ?Process $process = null; private bool $canceled = false; - public function __construct(LoopInterface $loop, string $name, string $command) + public function __construct(private LoopInterface $loop, private string $command) { - $this->loop = $loop; - $this->name = $name; - $this->command = $command; - $this->deferred = new Deferred(); - } - - public function getName(): string - { - return $this->name; + $this->deferred = new Deferred(function (): void { + $this->cancel(); + }); } /** - * @return ExtendedPromiseInterface&CancellablePromiseInterface + * @return PromiseInterface */ - public function run(): CancellablePromiseInterface + public function run(): PromiseInterface { $tmpStdOutResource = tmpfile(); if ($tmpStdOutResource === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stdout.'); + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); } $tmpStdErrResource = tmpfile(); if ($tmpStdErrResource === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stderr.'); + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); } - $this->process = new Process($this->command, null, null, [ + $this->process = new Process($this->command, fds: [ 1 => $tmpStdOutResource, 2 => $tmpStdErrResource, ]); @@ -75,7 +64,7 @@ public function run(): CancellablePromiseInterface fclose($tmpStdErrResource); if ($exitCode === null) { - $this->deferred->reject(new \PHPStan\Process\ProcessCrashedException($stdOut . $stdErr)); + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); return; } @@ -84,21 +73,20 @@ public function run(): CancellablePromiseInterface return; } - $this->deferred->reject(new \PHPStan\Process\ProcessCrashedException($stdOut . $stdErr)); + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); }); - /** @var ExtendedPromiseInterface&CancellablePromiseInterface */ return $this->deferred->promise(); } - public function cancel(): void + private function cancel(): void { if ($this->process === null) { - throw new \PHPStan\ShouldNotHappenException('Cancelling process before running'); + throw new ShouldNotHappenException('Cancelling process before running'); } $this->canceled = true; $this->process->terminate(); - $this->deferred->reject(new \PHPStan\Process\ProcessCanceledException()); + $this->deferred->reject(new ProcessCanceledException()); } } diff --git a/src/Process/Runnable/Runnable.php b/src/Process/Runnable/Runnable.php deleted file mode 100644 index e25e9154cc..0000000000 --- a/src/Process/Runnable/Runnable.php +++ /dev/null @@ -1,16 +0,0 @@ - */ - private array $queue = []; - - /** @var SplObjectStorage */ - private SplObjectStorage $running; - - public function __construct(RunnableQueueLogger $logger, int $maxSize) - { - $this->logger = $logger; - $this->maxSize = $maxSize; - - /** @var SplObjectStorage $running */ - $running = new SplObjectStorage(); - $this->running = $running; - } - - public function getQueueSize(): int - { - $allSize = 0; - foreach ($this->queue as [$runnable, $size, $deferred]) { - $allSize += $size; - } - - return $allSize; - } - - public function getRunningSize(): int - { - $allSize = 0; - foreach ($this->running as $running) { // phpcs:ignore - [$size] = $this->running->getInfo(); - $allSize += $size; - } - - return $allSize; - } - - public function queue(Runnable $runnable, int $size): CancellablePromiseInterface - { - if ($size > $this->maxSize) { - throw new \PHPStan\ShouldNotHappenException('Runnable size exceeds queue maxSize.'); - } - - $deferred = new Deferred(static function () use ($runnable): void { - $runnable->cancel(); - }); - $this->queue[] = [$runnable, $size, $deferred]; - $this->drainQueue(); - - /** @var CancellablePromiseInterface */ - return $deferred->promise(); - } - - private function drainQueue(): void - { - if (count($this->queue) === 0) { - $this->logger->log('Queue empty'); - return; - } - - $currentQueueSize = $this->getRunningSize(); - if ($currentQueueSize > $this->maxSize) { - throw new \PHPStan\ShouldNotHappenException('Running overflow'); - } - - if ($currentQueueSize === $this->maxSize) { - $this->logger->log('Queue is full'); - return; - } - - $this->logger->log('Queue not full - looking at first item in the queue'); - - [$runnable, $runnableSize, $deferred] = $this->queue[0]; - - $newSize = $currentQueueSize + $runnableSize; - if ($newSize > $this->maxSize) { - $this->logger->log( - sprintf( - 'Canot remote first item from the queue - it has size %d, current queue size is %d, new size would be %d', - $runnableSize, - $currentQueueSize, - $newSize - ) - ); - return; - } - - $this->logger->log(sprintf('Removing top item from queue - new size is %d', $newSize)); - - /** @var array{Runnable, int, Deferred} $popped */ - $popped = array_shift($this->queue); - if ($popped[0] !== $runnable || $popped[1] !== $runnableSize || $popped[2] !== $deferred) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $this->running->attach($runnable, [$runnableSize, $deferred]); - $this->logger->log(sprintf('Running process %s', $runnable->getName())); - $runnable->run()->then(function ($value) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished successfully', $runnable->getName())); - $deferred->resolve($value); - $this->running->detach($runnable); - $this->drainQueue(); - }, function (\Throwable $e) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished unsuccessfully: %s', $runnable->getName(), $e->getMessage())); - $deferred->reject($e); - $this->running->detach($runnable); - $this->drainQueue(); - }); - } - - public function cancelAll(): void - { - foreach ($this->queue as [$runnable, $size, $deferred]) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - - $runningDeferreds = []; - foreach ($this->running as $running) { // phpcs:ignore - [,$deferred] = $this->running->getInfo(); - $runningDeferreds[] = $deferred; - } - - foreach ($runningDeferreds as $deferred) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - } - -} diff --git a/src/Process/Runnable/RunnableQueueLogger.php b/src/Process/Runnable/RunnableQueueLogger.php deleted file mode 100644 index a29ada14b3..0000000000 --- a/src/Process/Runnable/RunnableQueueLogger.php +++ /dev/null @@ -1,10 +0,0 @@ - + */ + public function getAllowedSubTypes(ClassReflection $classReflection): array; + +} diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index c63f2c4108..7fe7ccd14e 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,23 +66,29 @@ 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(), - null, - $this->parameters, - $this->isVariadic, - $this->returnType - ), - ]; - } - return $this->variants; + return $this->variants ??= [ + new ExtendedFunctionVariant( + $this->templateTypeMap, + null, + $this->parameters, + $this->isVariadic, + $this->returnType, + $this->returnType, + new MixedType(), + ), + ]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; } public function isDeprecated(): TrinaryLogic @@ -118,18 +106,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 +144,48 @@ 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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index acbaaaac09..a64f9ac52e 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,54 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } 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 40271a382b..e1da082ca1 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,15 +2,23 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; - -class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension +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 { - /** @var MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $methods = []; public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -23,10 +31,10 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $this->methods[$classReflection->getCacheKey()][$methodName] = $method; } - return isset($this->methods[$classReflection->getCacheKey()][$methodName]); + return true; } - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { return $this->methods[$classReflection->getCacheKey()][$methodName]; } @@ -34,8 +42,8 @@ public function getMethod(ClassReflection $classReflection, string $methodName): private function findClassReflectionWithMethod( ClassReflection $classReflection, ClassReflection $declaringClass, - string $methodName - ): ?MethodReflection + string $methodName, + ): ?ExtendedMethodReflection { $methodTags = $classReflection->getMethodTags(); if (isset($methodTags[$methodName])) { @@ -47,20 +55,36 @@ private function findClassReflectionWithMethod( $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), - $parameterTag->getDefaultValue() + $parameterTag->getDefaultValue(), ); } + $templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $methodTags[$methodName]->getTemplateTags(), + )); + + $isStatic = $methodTags[$methodName]->isStatic(); + $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; + return new AnnotationMethodReflection( $methodName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $methodTags[$methodName]->getReturnType(), - $classReflection->getActiveTemplateTypeMap() + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ), $parameters, - $methodTags[$methodName]->isStatic(), - $this->detectMethodVariadic($parameters) + $isStatic, + $this->detectMethodVariadic($parameters), + $classReflection->hasNativeMethod($nativeCallMethodName) + ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() + : null, + $templateTypeMap, ); } @@ -73,21 +97,12 @@ private function findClassReflectionWithMethod( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithMethod($parentClass, $parentClass, $methodName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $parentClass, $methodName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - continue; + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; } - - return $methodWithDeclaringClass; } foreach ($classReflection->getInterfaces() as $interfaceClass) { @@ -104,7 +119,6 @@ private function findClassReflectionWithMethod( /** * @param AnnotationsMethodParameterReflection[] $parameters - * @return bool */ private function detectMethodVariadic(array $parameters): bool { diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index f48da8542e..cf39bebec9 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -3,14 +3,16 @@ namespace PHPStan\Reflection\Annotations; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NeverType; -class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - /** @var PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $properties = []; public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -23,10 +25,10 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa $this->properties[$classReflection->getCacheKey()][$propertyName] = $property; } - return isset($this->properties[$classReflection->getCacheKey()][$propertyName]); + return true; } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { return $this->properties[$classReflection->getCacheKey()][$propertyName]; } @@ -34,19 +36,40 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa private function findClassReflectionWithProperty( ClassReflection $classReflection, ClassReflection $declaringClass, - string $propertyName - ): ?PropertyReflection + string $propertyName, + ): ?ExtendedPropertyReflection { $propertyTags = $classReflection->getPropertyTags(); if (isset($propertyTags[$propertyName])) { + $propertyTag = $propertyTags[$propertyName]; + + $isReadable = $propertyTags[$propertyName]->isReadable(); + $isWritable = $propertyTags[$propertyName]->isWritable(); + if ($classReflection->hasNativeProperty($propertyName)) { + $nativeProperty = $classReflection->getNativeProperty($propertyName); + if (!$nativeProperty->isPrivate() && !$nativeProperty->isStatic()) { + $isReadable = $isReadable || $nativeProperty->isReadable(); + $isWritable = $isWritable || $nativeProperty->isWritable(); + } + } + return new AnnotationPropertyReflection( + $propertyName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( - $propertyTags[$propertyName]->getType(), - $classReflection->getActiveTemplateTypeMap() + $propertyTag->getReadableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ), - $propertyTags[$propertyName]->isReadable(), - $propertyTags[$propertyName]->isWritable() + TemplateTypeHelper::resolveTemplateTypes( + $propertyTag->getWritableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ), + $isReadable, + $isWritable, ); } @@ -59,21 +82,12 @@ private function findClassReflectionWithProperty( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithProperty($parentClass, $parentClass, $propertyName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $parentClass, $propertyName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - continue; + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; } - - return $methodWithDeclaringClass; } foreach ($classReflection->getInterfaces() as $interfaceClass) { diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php new file mode 100644 index 0000000000..a1f7ebfa6d --- /dev/null +++ b/src/Reflection/Assertions.php @@ -0,0 +1,111 @@ +asserts; + } + + /** + * @return AssertTag[] + */ + public function getAsserts(): array + { + return array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::NULL); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfTrue(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE && !$assert->isEquality()), + ), + ); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfFalse(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE && !$assert->isEquality()), + ), + ); + } + + /** + * @param callable(Type): Type $callable + */ + public function mapTypes(callable $callable): self + { + $assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType())); + + return new self(array_map($assertTagsCallback, $this->asserts)); + } + + public function intersectWith(Assertions $other): self + { + return new self(array_merge($this->getAll(), $other->getAll())); + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + public static function createFromResolvedPhpDocBlock(ResolvedPhpDocBlock $phpDocBlock): self + { + $tags = $phpDocBlock->getAssertTags(); + if (count($tags) === 0) { + return self::createEmpty(); + } + + return new self($tags); + } + +} diff --git a/src/Reflection/AttributeReflection.php b/src/Reflection/AttributeReflection.php new file mode 100644 index 0000000000..74d6874223 --- /dev/null +++ b/src/Reflection/AttributeReflection.php @@ -0,0 +1,33 @@ + $argumentTypes + */ + public function __construct(private string $name, private array $argumentTypes) + { + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return array + */ + public function getArgumentTypes(): array + { + return $this->argumentTypes; + } + +} diff --git a/src/Reflection/AttributeReflectionFactory.php b/src/Reflection/AttributeReflectionFactory.php new file mode 100644 index 0000000000..5cd5cd9cb9 --- /dev/null +++ b/src/Reflection/AttributeReflectionFactory.php @@ -0,0 +1,136 @@ + $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 f26041e01a..418177449f 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -2,124 +2,109 @@ 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\Exception\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\Broker\AnonymousClassNameHelper; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\ConstantNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\NonAutowiredService; 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; - -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; + +#[NonAutowiredService(name: 'betterReflectionProvider')] +final class BetterReflectionProvider implements ReflectionProvider { - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private \PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - private \PHPStan\BetterReflection\Reflector\ClassReflector $classReflector; - - private \PHPStan\BetterReflection\Reflector\FunctionReflector $functionReflector; - - private \PHPStan\BetterReflection\Reflector\ConstantReflector $constantReflector; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private PhpVersion $phpVersion; - - private \PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider $nativeFunctionReflectionProvider; - - private StubPhpDocProvider $stubPhpDocProvider; - - private \PHPStan\Reflection\FunctionReflectionFactory $functionReflectionFactory; - - private RelativePathHelper $relativePathHelper; - - private AnonymousClassNameHelper $anonymousClassNameHelper; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\File\FileHelper $fileHelper; - - private PhpStormStubsSourceStubber $phpstormStubsSourceStubber; - - /** @var \PHPStan\Reflection\FunctionReflection[] */ + /** @var FunctionReflection[] */ private array $functionReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private array $classReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private static array $anonymousClasses = []; - /** @var array */ + /** @var array */ private array $cachedConstants = []; + /** + * @param list $universalObjectCratesClasses + */ public function __construct( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - ClassReflector $classReflector, - FileTypeMapper $fileTypeMapper, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - PhpVersion $phpVersion, - NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, - StubPhpDocProvider $stubPhpDocProvider, - FunctionReflectionFactory $functionReflectionFactory, - RelativePathHelper $relativePathHelper, - AnonymousClassNameHelper $anonymousClassNameHelper, - Standard $printer, - FileHelper $fileHelper, - FunctionReflector $functionReflector, - ConstantReflector $constantReflector, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + #[AutowiredParameter(ref: '@betterReflectionReflector')] + 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, + #[AutowiredParameter(ref: '%universalObjectCratesClasses%')] + private array $universalObjectCratesClasses, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->classReflector = $classReflector; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->phpVersion = $phpVersion; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->relativePathHelper = $relativePathHelper; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; - $this->printer = $printer; - $this->fileHelper = $fileHelper; - $this->functionReflector = $functionReflector; - $this->constantReflector = $constantReflector; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; } public function hasClass(string $className): bool @@ -133,11 +118,11 @@ public function hasClass(string $className): bool } try { - $this->classReflector->reflect($className); + $this->reflector->reflectClass($className); return true; - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { return false; - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { return false; } } @@ -149,9 +134,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()); @@ -160,19 +145,30 @@ 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()->getPhpClassReflectionExtension(), $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($reflectionClass->getName()) + $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()), + $this->universalObjectCratesClasses, ); $this->classReflections[$reflectionClassName] = $classReflection; @@ -183,27 +179,22 @@ 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 supportsAnonymousClasses(): bool - { - return true; - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$scope->isInTrait()) { @@ -218,51 +209,84 @@ 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 = \PHPStan\BetterReflection\Reflection\ReflectionClass::createFromNode( - $this->classReflector, + $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()->getPhpClassReflectionExtension(), $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); @@ -270,6 +294,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; @@ -281,21 +309,28 @@ 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; $isPure = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); + $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; + $phpDocComment = null; + $phpDocParameterOutTags = []; + $phpDocParameterImmediatelyInvokedCallable = []; + $phpDocParameterClosureThisTypeTags = []; + + $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { $docComment = $reflectionFunction->getDocComment(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($reflectionFunction->getFileName(), null, null, $reflectionFunction->getName(), $docComment); @@ -303,42 +338,60 @@ private function getCustomFunction(string $functionName): \PHPStan\Reflection\Ph 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() !== false ? $reflectionFunction->getFileName() : null, - $isPure + $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 (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { // pass } @@ -346,72 +399,95 @@ public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scop 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); } if (array_key_exists($constantName, $this->cachedConstants)) { return $this->cachedConstants[$constantName]; } - $constantReflection = $this->constantReflector->reflect($constantName); - try { - $constantValue = $constantReflection->getValue(); - $constantValueType = ConstantTypeHelper::getTypeFromValue($constantValue); - $fileName = $constantReflection->getFileName(); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { - $constantValueType = new MixedType(); - $fileName = null; + $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 $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 (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { + // pass + } catch (InvalidIdentifierName) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $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 deleted file mode 100644 index 903fc7c02b..0000000000 --- a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php +++ /dev/null @@ -1,18 +0,0 @@ -parser = $parser; - $this->php8Parser = $php8Parser; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; - $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->container = $container; - $this->scanFiles = $scanFiles; - $this->scanDirectories = $scanDirectories; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; - $this->singleReflectionFile = $singleReflectionFile; - $this->staticReflectionClassNamePatterns = $staticReflectionClassNamePatterns; } public function create(): SourceLocator { - $locators = []; + $locators = [ + $this->optimizedSingleFileSourceLocatorRepository->getOrCreate( + PHP_VERSION_ID < 80500 + ? __DIR__ . '/../../../stubs/runtime/Attribute84.php' + : __DIR__ . '/../../../stubs/runtime/Attribute85.php', + ), + ]; if ($this->singleReflectionFile !== null) { $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($this->singleReflectionFile); } + $astLocator = new Locator($this->parser); + $locators[] = new AutoloadFunctionsSourceLocator( + new AutoloadSourceLocator($this->fileNodesFetcher, false), + new ReflectionClassSourceLocator( + $astLocator, + $this->reflectionSourceStubber, + ), + ); + $analysedDirectories = []; $analysedFiles = []; @@ -144,34 +113,48 @@ public function create(): SourceLocator $analysedDirectories[] = $analysedPath; } + $fileLocators = []; $analysedFiles = array_unique(array_merge($analysedFiles, $this->scanFiles)); foreach ($analysedFiles as $analysedFile) { - $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($analysedFile); + $fileLocators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($analysedFile); } $directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories)); foreach ($directories as $directory) { - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($directory); + $fileLocators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($directory); } - $astLocator = new Locator($this->parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); - - $astPhp8Locator = new Locator($this->php8Parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); + $astPhp8Locator = new Locator($this->php8Parser); - $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber)); - $locators[] = new ClassBlacklistSourceLocator($this->autoloadSourceLocator, $this->staticReflectionClassNamePatterns); foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { $locator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerAutoloaderProjectPath); if ($locator === null) { continue; } - $locators[] = $locator; + $fileLocators[] = $locator; } - $locators[] = new ClassWhitelistSourceLocator($this->autoloadSourceLocator, $this->staticReflectionClassNamePatterns); + + if (extension_loaded('phar')) { + $pharProtocolPath = Phar::running(); + if ($pharProtocolPath !== '') { + $mappings = [ + 'PHPStan\\BetterReflection\\' => [$pharProtocolPath . '/vendor/ondrejmirtes/better-reflection/src/'], + ]; + if ($this->playgroundMode) { + $mappings['PHPStan\\'] = [$pharProtocolPath . '/src/']; + } else { + $mappings['PHPStan\\Testing\\'] = [$pharProtocolPath . '/src/Testing/']; + } + $fileLocators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings($mappings), + ); + } + } + + $locators[] = new RewriteClassAliasSourceLocator(new AggregateSourceLocator($fileLocators)); + $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber)); + + $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); $locators[] = new PhpVersionBlacklistSourceLocator(new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php deleted file mode 100644 index d34a77a86f..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionClass for the specified $className. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionClass - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $className): Reflection - { - $lowerClassName = strtolower($className); - if (isset($this->reflections[$lowerClassName])) { - if ($this->reflections[$lowerClassName] instanceof \Throwable) { - throw $this->reflections[$lowerClassName]; - } - return $this->reflections[$lowerClassName]; - } - - try { - return $this->reflections[$lowerClassName] = parent::reflect($className); - } catch (\Throwable $e) { - $this->reflections[$lowerClassName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php deleted file mode 100644 index aca5db5d44..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php +++ /dev/null @@ -1,38 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionConstant for the specified $constantName. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionConstant - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $constantName): Reflection - { - if (isset($this->reflections[$constantName])) { - if ($this->reflections[$constantName] instanceof \Throwable) { - throw $this->reflections[$constantName]; - } - return $this->reflections[$constantName]; - } - - try { - return $this->reflections[$constantName] = parent::reflect($constantName); - } catch (\Throwable $e) { - $this->reflections[$constantName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php deleted file mode 100644 index 078392ef1b..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionFunction for the specified $functionName. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionFunction - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $functionName): Reflection - { - $lowerFunctionName = strtolower($functionName); - if (isset($this->reflections[$lowerFunctionName])) { - if ($this->reflections[$lowerFunctionName] instanceof \Throwable) { - throw $this->reflections[$lowerFunctionName]; - } - return $this->reflections[$lowerFunctionName]; - } - - try { - return $this->reflections[$lowerFunctionName] = parent::reflect($functionName); - } catch (\Throwable $e) { - $this->reflections[$lowerFunctionName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php new file mode 100644 index 0000000000..84d858e6b3 --- /dev/null +++ b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php @@ -0,0 +1,124 @@ + */ + private array $classReflections = []; + + /** @var array */ + private array $constantReflections = []; + + /** @var array */ + private array $functionReflections = []; + + public function __construct( + #[AutowiredParameter(ref: '@originalBetterReflectionReflector')] + private Reflector $reflector, + ) + { + } + + #[Override] + 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; + } + } + + #[Override] + 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; + } + } + + #[Override] + 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; + } + } + + #[Override] + public function reflectAllClasses(): iterable + { + return $this->reflector->reflectAllClasses(); + } + + #[Override] + public function reflectAllFunctions(): iterable + { + return $this->reflector->reflectAllFunctions(); + } + + #[Override] + 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..3da44c64bf --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php @@ -0,0 +1,61 @@ +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; + } + + #[Override] + 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 f88f060808..3c43f17c7c 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -2,10 +2,12 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\Const_; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -14,11 +16,29 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Reflection\ConstantNameHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ConstantTypeHelper; use ReflectionClass; use ReflectionFunction; use function array_key_exists; -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. @@ -28,44 +48,34 @@ * * 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; } + #[Override] 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; @@ -77,7 +87,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): if (!is_string($reflectionFileName)) { return null; } - if (!file_exists($reflectionFileName)) { + if (!is_file($reflectionFileName)) { return null; } @@ -85,68 +95,31 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } if ($identifier->isConstant()) { - $constantName = $identifier->getName(); - $nodeToReflection = new NodeToReflection(); - foreach ($this->constantNodes as $stmtConst) { - if ($stmtConst->getNode() instanceof FuncCall) { - $constantReflection = $nodeToReflection->__invoke( - $reflector, - $stmtConst->getNode(), - $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()) { @@ -154,63 +127,82 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } $loweredClassName = strtolower($identifier->getName()); - if (array_key_exists($loweredClassName, $this->classReflections)) { - return $this->classReflections[$loweredClassName]; + if (array_key_exists($loweredClassName, $this->presentSymbols['classes'])) { + $startLine = null; + if (array_key_exists($loweredClassName, $this->startLineByClass)) { + $startLine = $this->startLineByClass[$loweredClassName]; + } else { + $reflection = $this->getReflectionClass($identifier->getName()); + if ( + $reflection !== null + && $reflection->getStartLine() !== false + && is_string($reflection->getFileName()) + && is_file($reflection->getFileName()) + && $reflection->getFileName() === $this->presentSymbols['classes'][$loweredClassName] + ) { + $startLine = $reflection->getStartLine(); + } + } + return $this->findReflection($reflector, $this->presentSymbols['classes'][$loweredClassName], $identifier, $startLine); } - $locateResult = $this->locateClassByName($identifier->getName()); if ($locateResult === null) { - if (array_key_exists($loweredClassName, $this->classNodes)) { - foreach ($this->classNodes[$loweredClassName] as $classNode) { - $nodeToReflection = new NodeToReflection(); - return $this->classReflections[$loweredClassName] = $nodeToReflection->__invoke( - $reflector, - $classNode->getNode(), - $this->locatedSourcesByFile[$classNode->getFileName()], - $classNode->getNamespace() - ); - } - } return null; } - [$potentiallyLocatedFile, $className, $startLine] = $locateResult; + [$potentiallyLocatedFiles, $className, $startLine] = $locateResult; + if ($startLine !== null) { + $this->startLineByClass[strtolower($className)] = $startLine; + } + + $newIdentifier = new Identifier($className, $identifier->getType()); + + foreach ($potentiallyLocatedFiles as $potentiallyLocatedFile) { + $reflection = $this->findReflection($reflector, $potentiallyLocatedFile, $newIdentifier, $startLine); + if ($reflection === null) { + continue; + } + + return $reflection; + } - return $this->findReflection($reflector, $potentiallyLocatedFile, new Identifier($className, $identifier->getType()), $startLine); + 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) { + $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--; } @@ -219,11 +211,11 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ } } - return $this->classReflections[$identifierName] = $nodeToReflection->__invoke( + return $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $locatedSource, - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); } @@ -231,26 +223,81 @@ 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; } + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { return []; } + /** + * @return ReflectionClass|null + */ + private function getReflectionClass(string $className): ?ReflectionClass + { + if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { + return new ReflectionClass($className); + } + + return null; + } + /** * Attempt to locate a class by name. * @@ -264,30 +311,31 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id * that it cannot find the file, so we squelch the errors by overriding the * error handler temporarily. * - * @return array{string, string, int|null}|null + * @return array{string[], string, int|null}|null */ private function locateClassByName(string $className): ?array { - if (class_exists($className, 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) { @@ -303,14 +351,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(); } @@ -318,9 +379,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 3f56c2ab95..91f5fa1b6f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -2,35 +2,45 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\BuilderHelpers; +use Override; +use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; 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 + #[Override] + 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) { $fullClassName = $node->name->toString(); if ($this->currentNamespaceNode !== null && $this->currentNamespaceNode->name !== null) { @@ -39,67 +49,71 @@ public function enterNode(\PhpParser\Node $node): ?int $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 */ + /** @var Node\Scalar\String_ $nameNode */ $nameNode = $node->getArgs()[0]->value; $constantName = $nameNode->value; - if (defined($constantName)) { - $constantValue = constant($constantName); - $node->getArgs()[1]->value = BuilderHelpers::normalizeValue($constantValue); - } - $constantNode = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName + new LocatedSource($this->contents, $constantName, $this->fileName), ); - $this->constantNodes[] = $constantNode; + $this->constantNodes[ConstantNameHelper::normalize($constantName)][] = $constantNode; - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; } /** - * @param \PhpParser\Node $node * @return null */ - public function leaveNode(\PhpParser\Node $node) + #[Override] + public function leaveNode(Node $node) { if (!$node instanceof Namespace_) { return null; @@ -110,7 +124,7 @@ public function leaveNode(\PhpParser\Node $node) } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -118,7 +132,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -126,19 +140,20 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function reset(string $fileName): void + public function reset(string $fileName, string $contents): void { $this->classNodes = []; $this->functionNodes = []; $this->constantNodes = []; $this->fileName = $fileName; + $this->contents = $contents; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php deleted file mode 100644 index 9b4ca0d402..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php +++ /dev/null @@ -1,51 +0,0 @@ -sourceLocator = $sourceLocator; - $this->patterns = $patterns; - } - - public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection - { - if ($identifier->isClass()) { - foreach ($this->patterns as $pattern) { - if (Strings::match($identifier->getName(), $pattern) !== null) { - return null; - } - } - } - - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return $this->sourceLocator->locateIdentifiersByType($reflector, $identifierType); - } - -} diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php deleted file mode 100644 index cff56df029..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php +++ /dev/null @@ -1,53 +0,0 @@ -sourceLocator = $sourceLocator; - $this->patterns = $patterns; - } - - public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection - { - if ($identifier->isClass()) { - foreach ($this->patterns as $pattern) { - if (Strings::match($identifier->getName(), $pattern) !== null) { - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - } - - return null; - } - - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return $this->sourceLocator->locateIdentifiersByType($reflector, $identifierType); - } - -} diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index cc0e55dea6..1195311fd2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -3,121 +3,125 @@ 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\DependencyInjection\AutowiredService; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; +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 +#[AutowiredService] +final class ComposerJsonAndInstalledJsonSourceLocatorMaker { - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory; - - private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory; - public function __construct( - OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, - OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, - OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory + private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory, + private PhpVersion $phpVersion, ) { - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->optimizedPsrAutoloaderLocatorFactory = $optimizedPsrAutoloaderLocatorFactory; - $this->optimizedDirectorySourceLocatorFactory = $optimizedDirectorySourceLocatorFactory; } public function create(string $projectInstallationPath): ?SourceLocator { - $composerJsonPath = $projectInstallationPath . '/composer.json'; - if (!is_file($composerJsonPath)) { + $composer = ComposerHelper::getComposerConfig($projectInstallationPath); + + if ($composer === null) { return null; } - $installedJsonPath = $projectInstallationPath . '/vendor/composer/installed.json'; + + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($projectInstallationPath, $composer); + + $installedJsonPath = $vendorDirectory . '/composer/installed.json'; if (!is_file($installedJsonPath)) { return null; } $installedJsonDirectoryPath = dirname($installedJsonPath); - try { - $composerJsonContents = FileReader::read($composerJsonPath); - $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { - return null; - } - try { $installedJsonContents = FileReader::read($installedJsonPath); $installedJson = Json::decode($installedJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { return null; } $installed = $installedJson['packages'] ?? $installedJson; + $dev = (bool) ($installedJson['dev'] ?? true); $classMapPaths = array_merge( $this->prefixPaths($this->packageToClassMapPaths($composer), $projectInstallationPath . '/'), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixPaths( - $this->packageToClassMapPaths($package), - $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package) - ); - }, $installed) + $dev ? $this->prefixPaths($this->packageToClassMapPaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], + ...array_map(fn (array $package): array => $this->prefixPaths( + $this->packageToClassMapPaths($package), + $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory), + ), $installed), ); - $classMapFiles = array_filter($classMapPaths, 'is_file'); - $classMapDirectories = array_filter($classMapPaths, 'is_dir'); $filePaths = array_merge( $this->prefixPaths($this->packageToFilePaths($composer), $projectInstallationPath . '/'), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixPaths( - $this->packageToFilePaths($package), - $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package) - ); - }, $installed) + $dev ? $this->prefixPaths($this->packageToFilePaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], + ...array_map(fn (array $package): array => $this->prefixPaths( + $this->packageToFilePaths($package), + $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory), + ), $installed), ); $locators = []; $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr4Mapping::fromArrayMappings(array_merge_recursive( $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $projectInstallationPath), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr4AutoloadNamespaces($package), - $projectInstallationPath, - $installedJsonDirectoryPath, - $package - ); - }, $installed) - )) + $dev ? $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr4AutoloadNamespaces($package), + $installedJsonDirectoryPath, + $package, + $vendorDirectory, + ), $installed), + )), ); $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr0Mapping::fromArrayMappings(array_merge_recursive( $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $projectInstallationPath), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr0AutoloadNamespaces($package), - $projectInstallationPath, - $installedJsonDirectoryPath, - $package - ); - }, $installed) - )) + $dev ? $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr0AutoloadNamespaces($package), + $installedJsonDirectoryPath, + $package, + $vendorDirectory, + ), $installed), + )), ); - foreach ($classMapDirectories as $classMapDirectory) { - if (!is_dir($classMapDirectory)) { + $files = []; + foreach ($classMapPaths as $classMapPath) { + if (is_dir($classMapPath)) { + $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapPath); continue; } - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapDirectory); + if (!is_file($classMapPath)) { + continue; + } + $files[] = $classMapPath; } - - $files = []; - - foreach (array_merge($classMapFiles, $filePaths) as $file) { + foreach ($filePaths as $file) { if (!is_file($file)) { continue; } @@ -128,6 +132,39 @@ public function create(string $projectInstallationPath): ?SourceLocator $locators[] = $this->optimizedDirectorySourceLocatorFactory->createByFiles($files); } + $binDir = ComposerHelper::getBinDirFromComposerConfig($projectInstallationPath, $composer); + $phpunitBridgeDir = $binDir . '/.phpunit'; + if (!is_dir($vendorDirectory . '/phpunit/phpunit') && is_dir($phpunitBridgeDir)) { + // from https://github.com/composer/composer/blob/8ff237afb61b8766efa576b8ae1cc8560c8aed96/phpstan/locate-phpunit-autoloader.php + $bestDirFound = null; + $phpunitBridgeDirectories = glob($phpunitBridgeDir . '/phpunit-*', GLOB_ONLYDIR); + if ($phpunitBridgeDirectories !== false) { + foreach (array_reverse($phpunitBridgeDirectories) as $dir) { + $bestDirFound = $dir; + if ($this->phpVersion->getVersionId() >= 80100 && str_contains($dir, 'phpunit-10')) { + break; + } + if ($this->phpVersion->getVersionId() >= 80000) { + if (str_contains($dir, 'phpunit-9')) { + break; + } + continue; + } + + if (str_contains($dir, 'phpunit-8') || str_contains($dir, 'phpunit-7')) { + break; + } + } + + if ($bestDirFound !== null) { + $phpunitBridgeLocator = $this->create($bestDirFound); + if ($phpunitBridgeLocator !== null) { + $locators[] = $phpunitBridgeLocator; + } + } + } + } + return new AggregateSourceLocator($locators); } @@ -136,11 +173,9 @@ public function create(string $projectInstallationPath): ?SourceLocator * * @return array> */ - private function packageToPsr4AutoloadNamespaces(array $package): array + private function packageToPsr4AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-4'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-4'] ?? []); } /** @@ -148,11 +183,9 @@ private function packageToPsr4AutoloadNamespaces(array $package): array * * @return array> */ - private function packageToPsr0AutoloadNamespaces(array $package): array + private function packageToPsr0AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-0'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-0'] ?? []); } /** @@ -160,9 +193,9 @@ private function packageToPsr0AutoloadNamespaces(array $package): array * * @return array */ - private function packageToClassMapPaths(array $package): array + private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - return $package['autoload']['classmap'] ?? []; + return $package[$autoloadSection]['classmap'] ?? []; } /** @@ -170,25 +203,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 $projectInstallationPath, string $installedJsonDirectoryPath, - array $package + array $package, + string $vendorDirectory, ): string { if (array_key_exists('install-path', $package)) { return $installedJsonDirectoryPath . '/' . $package['install-path'] . '/'; } - return $projectInstallationPath . '/vendor/' . $package['name'] . '/'; + return $vendorDirectory . '/' . $package['name'] . '/'; } /** @@ -197,13 +230,11 @@ private function packagePrefixPath( * * @return array> */ - private function prefixWithPackagePath(array $paths, string $projectInstallationPath, string $installedJsonDirectoryPath, array $package): array + private function prefixWithPackagePath(array $paths, string $installedJsonDirectoryPath, array $package, string $vendorDirectory): array { - $prefix = $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package); + $prefix = $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory); - return array_map(function (array $paths) use ($prefix): array { - return $this->prefixPaths($paths, $prefix); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths); } /** @@ -213,9 +244,7 @@ private function prefixWithPackagePath(array $paths, string $projectInstallation */ private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array { - return array_map(function (array $paths) use ($trimmedInstallationPath): array { - return $this->prefixPaths($paths, $trimmedInstallationPath . '/'); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $trimmedInstallationPath . '/'), $paths); } /** @@ -225,9 +254,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 5791a790b6..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 PHPStan\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 \PHPStan\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource; - /** - * @param array>> $classNodes - * @param array> $functionNodes - * @param array> $constantNodes - * @param \PHPStan\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource + * @param array>> $classNodes + * @param array>> $functionNodes + * @param array>> $constantNodes */ public function __construct( - array $classNodes, - array $functionNodes, - array $constantNodes, - LocatedSource $locatedSource + private array $classNodes, + private array $functionNodes, + private array $constantNodes, ) { - $this->classNodes = $classNodes; - $this->functionNodes = $functionNodes; - $this->constantNodes = $constantNodes; - $this->locatedSource = $locatedSource; } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -46,7 +29,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -54,16 +37,11 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function getLocatedSource(): LocatedSource - { - return $this->locatedSource; - } - } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index db88b2b177..1258607a9f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -3,24 +3,22 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PhpParser\NodeTraverser; -use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileReader; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; -class FileNodesFetcher +#[AutowiredService] +final class FileNodesFetcher { - private \PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor $cachingVisitor; - - private Parser $parser; - public function __construct( - CachingVisitor $cachingVisitor, - Parser $parser + private CachingVisitor $cachingVisitor, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + private Parser $parser, ) { - $this->cachingVisitor = $cachingVisitor; - $this->parser = $parser; } public function fetchNodes(string $fileName): FetchedNodesResult @@ -29,23 +27,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->parser->parseFile($fileName); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - return new FetchedNodesResult([], [], [], $locatedSource); + } catch (ParserErrorsException) { + return new FetchedNodesResult([], [], []); } - $this->cachingVisitor->reset($fileName); + $this->cachingVisitor->reset($fileName, $contents); $nodeTraverser->traverse($ast); - return new FetchedNodesResult( + $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 7efba32de1..047a6808e2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -2,10 +2,18 @@ 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 @@ -29,7 +37,8 @@ final class FileReadTrapStreamWrapper /** @var string[]|null */ private static ?array $registeredStreamWrapperProtocols; - public static ?string $autoloadLocatedFile = null; + /** @var string[] */ + public static array $autoloadLocatedFiles = []; private bool $readFromFile = false; @@ -46,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) { @@ -66,7 +75,7 @@ public static function withStreamWrapperOverride( } self::$registeredStreamWrapperProtocols = null; - self::$autoloadLocatedFile = null; + self::$autoloadLocatedFiles = []; return $result; } @@ -88,11 +97,15 @@ public static function withStreamWrapperOverride( */ public function stream_open($path, $mode, $options, &$openedPath): bool { - self::$autoloadLocatedFile = $path; + $exists = is_file($path) || (stream_resolve_include_path($path) !== false); + + if ($exists) { + self::$autoloadLocatedFiles[] = $path; + } $this->readFromFile = false; $this->seekPosition = 0; - return true; + return $exists; } /** @@ -101,7 +114,6 @@ public function stream_open($path, $mode, $options, &$openedPath): bool * * @param int $count * - * @return string */ public function stream_read($count): string { @@ -117,7 +129,6 @@ public function stream_read($count): string * Since we allowed the open to succeed, we should allow the close to occur * as well. * - * @return void */ public function stream_close(): void { @@ -134,11 +145,11 @@ public function stream_close(): void */ public function stream_stat() { - if (self::$autoloadLocatedFile === null) { + if (self::$autoloadLocatedFiles === []) { return false; } - return $this->url_stat(self::$autoloadLocatedFile, STREAM_URL_STAT_QUIET); + return $this->url_stat(self::$autoloadLocatedFiles[0], STREAM_URL_STAT_QUIET); } /** @@ -158,20 +169,31 @@ public function stream_stat() * @return mixed[]|bool */ public function url_stat($path, $flags) + { + return $this->invokeWithRealFileStreamWrapper(static function ($path, $flags) { + if (($flags & STREAM_URL_STAT_QUIET) !== 0) { + return @stat($path); + } + + return stat($path); + }, [$path, $flags]); + } + + /** + * @param mixed[] $args + * @return mixed + */ + private function invokeWithRealFileStreamWrapper(callable $cb, array $args) { if (self::$registeredStreamWrapperProtocols === null) { - throw new \PHPStan\ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); + throw new ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); } foreach (self::$registeredStreamWrapperProtocols as $protocol) { 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); @@ -184,13 +206,15 @@ public function url_stat($path, $flags) /** * Simulates behavior of reading from an empty file. * - * @return bool */ public function stream_eof(): bool { return $this->readFromFile; } + /** + * @return true + */ public function stream_flush(): bool { return true; @@ -204,7 +228,6 @@ public function stream_tell(): int /** * @param int $offset * @param int $whence - * @return bool */ public function stream_seek($offset, $whence): bool { @@ -234,11 +257,22 @@ public function stream_seek($offset, $whence): bool * @param int $option * @param int $arg1 * @param int $arg2 - * @return bool + * + * @return false */ 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 f95853719b..73d23ac817 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -2,137 +2,127 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; +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; - - /** @var string[] */ - private array $files; - - /** @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 FileNodesFetcher $fileNodesFetcher - * @param string[] $files + * @param array $classToFile + * @param array> $functionToFiles + * @param array $constantToFile */ public function __construct( - FileNodesFetcher $fileNodesFetcher, - array $files + private FileNodesFetcher $fileNodesFetcher, + private array $classToFile, + private array $functionToFiles, + private array $constantToFile, ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->files = $files; } + #[Override] 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)) { + 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 (!array_key_exists($functionName, $this->functionNodes)) { + if ($fetchedFunctionNode === null) { return null; } - return $this->nodeToReflection($reflector, $this->functionNodes[$functionName]); + return $this->nodeToReflection($reflector, $fetchedFunctionNode); + } + + if ($identifier->isConstant()) { + $constantName = ConstantNameHelper::normalize($identifier->getName()); + $file = $this->findFileByConstant($constantName); + + if ($file === null) { + return null; + } + + $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); + + if (!array_key_exists($constantName, $fetchedConstantNodes)) { + return null; + } + + /** @var FetchedNode $fetchedConstantNode */ + $fetchedConstantNode = current($fetchedConstantNodes[$constantName]); + + return $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $constantName), + ); } return null; } /** - * @param 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 +130,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,149 +151,70 @@ private function findFilesByFunction(string $functionName): array return $this->functionToFiles[$functionName]; } - private function init(): void + /** + * @return list + */ + #[Override] + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - $classToFile = []; - $functionToFiles = []; - foreach ($this->files as $file) { - $symbols = $this->findSymbols($file); - $classesInFile = $symbols['classes']; - $functionsInFile = $symbols['functions']; - foreach ($classesInFile as $classInFile) { - $classToFile[$classInFile] = $file; + $reflections = []; + if ($identifierType->isClass()) { + foreach ($this->classToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { + foreach ($fetchedClassNodes as $fetchedClassNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); + } + } } - foreach ($functionsInFile as $functionInFile) { - if (!array_key_exists($functionInFile, $functionToFiles)) { - $functionToFiles[$functionInFile] = []; + } elseif ($identifierType->isFunction()) { + foreach ($this->functionToFiles as $files) { + foreach ($files as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { + foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); + continue 2; + } + } + } + } + } elseif ($identifierType->isConstant()) { + foreach ($this->constantToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { + foreach ($fetchedConstantNodes as $fetchedConstantNode) { + $reflections[$identifierName] = $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), + ); + } } - $functionToFiles[$functionInFile][] = $file; } } - $this->classToFile = $classToFile; - $this->functionToFiles = $functionToFiles; + return array_values($reflections); } - /** - * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() - * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 - * - * @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' => []]; - } - - // strip heredocs/nowdocs - $heredocRegex = '{ - # opening heredoc/nowdoc delimiter (word-chars) - <<<[ \t]*+([\'"]?)(\w++)\\1 - # needs to be followed by a newline - (?:\r\n|\n|\r) - # the meat of it, matching line by line until end delimiter - (?: - # a valid line is optional white-space (possessive match) not followed by the end delimiter, then anything goes for the rest of the line - [\t ]*+(?!\\2 \b)[^\r\n]*+ - # end of line(s) - [\r\n]++ - )* - # end delimiter - [\t ]*+ \\2 (?=\b) - }x'; - - // run first assuming the file is valid unicode - $contentWithoutHeredoc = preg_replace($heredocRegex . 'u', 'null', $contents); - if ($contentWithoutHeredoc === null) { - // run again without unicode support if the file failed to be parsed - $contents = preg_replace($heredocRegex, 'null', $contents); - } else { - $contents = $contentWithoutHeredoc; + if ($constantNode instanceof Node\Expr\FuncCall) { + return null; } - unset($contentWithoutHeredoc); - if ($contents === null) { - return ['classes' => [], 'functions' => []]; - } - // strip strings - $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); - if ($contents === null) { - return ['classes' => [], 'functions' => []]; - } - // strip leading non-php code if needed - if (strpos($contents, ' [], 'functions' => []]; - } - if ($replacements === 0) { - return ['classes' => [], 'functions' => []]; - } - } - // strip non-php blocks in the file - $contents = preg_replace('{\?>(?:[^<]++|<(?!\?))*+<\?}s', '?> [], 'functions' => []]; - } - // strip trailing non-php code if needed - $pos = strrpos($contents, '?>'); - if ($pos !== false && strpos(substr($contents, $pos), ' [], 'functions' => []]; + /** @var int $position */ + foreach ($constantNode->consts as $position => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); } - } - preg_match_all('{ - (?: - \b(?])(?Pclass|interface|trait|function) \s++ (?P&\s*)? (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) - | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] - ) - }ix', $contents, $matches); - - $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 []; + throw new ShouldNotHappenException(); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index 71aee274ae..5803517e63 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -2,39 +2,219 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\Cache\Cache; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileFinder; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ConstantNameHelper; +use function array_key_exists; +use function count; +use function in_array; +use function ltrim; +use function php_strip_whitespace; +use function preg_match_all; +use function preg_replace; +use function sha1_file; +use function sprintf; +use function strtolower; -class OptimizedDirectorySourceLocatorFactory +#[AutowiredService] +final class OptimizedDirectorySourceLocatorFactory { - private FileNodesFetcher $fileNodesFetcher; + private PhpFileCleaner $cleaner; - private FileFinder $fileFinder; + private string $extraTypes; - public function __construct(FileNodesFetcher $fileNodesFetcher, FileFinder $fileFinder) + public function __construct( + private FileNodesFetcher $fileNodesFetcher, + #[AutowiredParameter(ref: '@fileFinderScan')] + private FileFinder $fileFinder, + private PhpVersion $phpVersion, + private Cache $cache, + ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileFinder = $fileFinder; + $this->extraTypes = $this->phpVersion->supportsEnums() ? '|enum' : ''; + $this->cleaner = new PhpFileCleaner(); } public function createByDirectory(string $directory): OptimizedDirectorySourceLocator { + $files = $this->fileFinder->findFiles([$directory])->getFiles(); + $fileHashes = []; + foreach ($files as $file) { + $hash = sha1_file($file); + if ($hash === false) { + continue; + } + $fileHashes[$file] = $hash; + } + + $cacheKey = sprintf('odsl-%s', $directory); + $variableCacheKey = 'v1'; + + /** @var array|null $cached */ + $cached = $this->cache->load($cacheKey, $variableCacheKey); + if ($cached !== null) { + foreach ($cached as $file => [$hash, $classes, $functions, $constants]) { + if (!array_key_exists($file, $fileHashes)) { + unset($cached[$file]); + continue; + } + $newHash = $fileHashes[$file]; + unset($fileHashes[$file]); + if ($hash === $newHash) { + continue; + } + + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + } else { + $cached = []; + } + + foreach ($fileHashes as $file => $newHash) { + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + $this->cache->save($cacheKey, $variableCacheKey, $cached); + + [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($cached); + return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->fileFinder->findFiles([$directory])->getFiles() + $classToFile, + $functionToFiles, + $constantToFile, ); } /** * @param string[] $files - * @return OptimizedDirectorySourceLocator */ 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, - $files + $classToFile, + $functionToFiles, + $constantToFile, ); } + /** + * @param array $symbols + * @return array{array, array>, array} + */ + private function changeStructure(array $symbols): array + { + $classToFile = []; + $constantToFile = []; + $functionToFiles = []; + foreach ($symbols as $file => [, $classes, $functions, $constants]) { + foreach ($classes as $classInFile) { + $classToFile[$classInFile] = $file; + } + foreach ($functions as $functionInFile) { + if (!array_key_exists($functionInFile, $functionToFiles)) { + $functionToFiles[$functionInFile] = []; + } + $functionToFiles[$functionInFile][] = $file; + } + foreach ($constants as $constantInFile) { + $constantToFile[$constantInFile] = $file; + } + } + + return [ + $classToFile, + $functionToFiles, + $constantToFile, + ]; + } + + /** + * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() + * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 + * + * @return array{string[], string[], string[]} + */ + private function findSymbols(string $file): array + { + $contents = @php_strip_whitespace($file); + if ($contents === '') { + return [[], [], []]; + } + + $matchResults = (bool) preg_match_all(sprintf('{\b(?:(?:class|interface|trait|const|function%s)\s)|(?:define\s*\()}i', $this->extraTypes), $contents, $matches); + if (!$matchResults) { + return [[], [], []]; + } + + $contents = $this->cleaner->clean($contents, count($matches[0])); + + preg_match_all(sprintf('{ + (?: + \b(?])(?: + (?: (?Pclass|interface|trait%s) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) ) + | (?: (?Pfunction) \s++ (?:&\s*)? (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [&\(] ) + | (?: (?Pconst) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [^;] ) + | (?: (?:\\\)? (?Pdefine) \s*+ \( \s*+ [\'"] (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:[\\\\]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+) ) + | (?: (?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] ) + ) + ) + }ix', $this->extraTypes), $contents, $matches); + + $classes = []; + $functions = []; + $constants = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') { + $namespace = preg_replace('~\s+~', '', strtolower($matches['nsname'][$i])) . '\\'; + continue; + } + + if ($matches['function'][$i] !== '') { + $functions[] = strtolower(ltrim($namespace . $matches['fname'][$i], '\\')); + continue; + } + + if ($matches['constant'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize(ltrim($namespace . $matches['cname'][$i], '\\')); + } + + if ($matches['define'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize($matches['dname'][$i]); + continue; + } + + $name = $matches['name'][$i]; + + // skip anon classes extending/implementing + if (in_array($name, ['extends', 'implements'], true)) { + continue; + } + + $classes[] = strtolower(ltrim($namespace . $name, '\\')); + } + + return [ + $classes, + $functions, + $constants, + ]; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php index 9f385a6d17..e0404ad629 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -2,17 +2,18 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -class OptimizedDirectorySourceLocatorRepository -{ +use PHPStan\DependencyInjection\AutowiredService; +use function array_key_exists; - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory $factory; +#[AutowiredService] +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 diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index b065a1f7ef..e2768bc6d7 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -2,41 +2,55 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; 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 PHPStan\DependencyInjection\GenerateFactory; +use function is_file; -class OptimizedPsrAutoloaderLocator implements SourceLocator +#[GenerateFactory(interface: OptimizedPsrAutoloaderLocatorFactory::class)] +final class OptimizedPsrAutoloaderLocator implements SourceLocator { - private PsrAutoloaderMapping $mapping; - - 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; } + #[Override] 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,8 +58,9 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @return Reflection[] + * @return list */ + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { return []; diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index 25904df254..b75b4c7f66 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -2,7 +2,8 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node\Expr\FuncCall; +use Override; +use PhpParser\Node\Stmt\Const_; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -12,33 +13,72 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\Reflection\ConstantNameHelper; +use PHPStan\ShouldNotHappenException; +use function array_key_exists; +use function array_keys; +use function strtolower; -class OptimizedSingleFileSourceLocator implements SourceLocator +#[GenerateFactory(interface: OptimizedSingleFileSourceLocatorFactory::class)] +final class OptimizedSingleFileSourceLocator implements SourceLocator { - private \PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher $fileNodesFetcher; - - private string $fileName; - - 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; } + #[Override] 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 +88,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 +100,166 @@ 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(); + } + + #[Override] + 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 []; + return $reflections; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php index baaad91822..b97c230f79 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php @@ -2,17 +2,18 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -class OptimizedSingleFileSourceLocatorRepository -{ +use PHPStan\DependencyInjection\AutowiredService; +use function array_key_exists; - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorFactory $factory; +#[AutowiredService] +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..137941d004 --- /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, offset: $offset ?? $this->index) === 1; + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php index 9d9b7ec942..3ff2e8cc96 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -9,22 +10,17 @@ use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -class PhpVersionBlacklistSourceLocator implements SourceLocator +final class PhpVersionBlacklistSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - public function __construct( - SourceLocator $sourceLocator, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber + private SourceLocator $sourceLocator, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, ) { - $this->sourceLocator = $sourceLocator; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { @@ -42,6 +38,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $this->sourceLocator->locateIdentifier($reflector, $identifier); } + #[Override] 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..d35b677a1a --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php @@ -0,0 +1,56 @@ +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)), + ); + } + + #[Override] + 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..6be98f2ba5 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php @@ -0,0 +1,49 @@ +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); + } + + #[Override] + 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 2afa00d549..8e1da7dec0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php @@ -2,22 +2,23 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; 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; -class SkipClassAliasSourceLocator implements SourceLocator +final class SkipClassAliasSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - public function __construct(SourceLocator $sourceLocator) + public function __construct(private SourceLocator $sourceLocator) { - $this->sourceLocator = $sourceLocator; } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { @@ -26,7 +27,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); } @@ -37,6 +41,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $this->sourceLocator->locateIdentifier($reflector, $identifier); } + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { return $this->sourceLocator->locateIdentifiersByType($reflector, $identifierType); diff --git a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php b/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php deleted file mode 100644 index c482ee70ef..0000000000 --- a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php +++ /dev/null @@ -1,62 +0,0 @@ -getExtensionFromFilePath($relativeFilePath), $file); - } - - public function generateFunctionStub(string $functionName): ?StubData - { - $lowerFunctionName = strtolower($functionName); - if (!array_key_exists($lowerFunctionName, Php8StubsMap::FUNCTIONS)) { - return null; - } - - $relativeFilePath = Php8StubsMap::FUNCTIONS[$lowerFunctionName]; - $file = self::DIRECTORY . '/' . $relativeFilePath; - - return new StubData(FileReader::read($file), $this->getExtensionFromFilePath($relativeFilePath), $file); - } - - public function generateConstantStub(string $constantName): ?StubData - { - return null; - } - - private function getExtensionFromFilePath(string $relativeFilePath): string - { - $pathParts = explode('/', $relativeFilePath); - if ($pathParts[1] === 'Zend') { - return 'Core'; - } - - return $pathParts[2]; - } - -} diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php new file mode 100644 index 0000000000..4049733f0f --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -0,0 +1,30 @@ +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..0a1f1e9e6a --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php @@ -0,0 +1,23 @@ +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..2d3920e771 --- /dev/null +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php @@ -0,0 +1,104 @@ +getName(), [ + 'getFileName', + 'getStartLine', + 'getEndLine', + 'getDocComment', + 'getReflectionConstant', + 'getParentClass', + 'getExtensionName', + 'getBackingType', + ], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + if (in_array($methodReflection->getName(), ['getFileName', 'getExtensionName'], true)) { + return new UnionType([ + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getDocComment') { + return new UnionType([ + new StringType(), + new ConstantBooleanType(false), + ]); + } + + if (in_array($methodReflection->getName(), ['getStartLine', 'getEndLine'], true)) { + return new IntegerType(); + } + + if ($methodReflection->getName() === 'getReflectionConstant') { + return new UnionType([ + new ObjectType(ReflectionClassConstant::class), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getParentClass') { + return new UnionType([ + new ObjectType(ReflectionClass::class), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getBackingType') { + return new UnionType([ + new ObjectType(ReflectionNamedType::class), + new NullType(), + ]); + } + + return null; + } + +} diff --git a/src/Reflection/BrokerAwareExtension.php b/src/Reflection/BrokerAwareExtension.php deleted file mode 100644 index 9ca104d127..0000000000 --- a/src/Reflection/BrokerAwareExtension.php +++ /dev/null @@ -1,16 +0,0 @@ - new self($function, $variant), $variants); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getResolvedTemplateTypeMap(); + } + + /** + * @return list + */ + public function getParameters(): array + { + return $this->variant->getParameters(); + } + + public function isVariadic(): bool + { + return $this->variant->isVariadic(); + } + + public function getReturnType(): Type + { + return $this->variant->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->variant->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->variant->getNativeReturnType(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->variant->getCallSiteVarianceMap(); + } + + public function getThrowPoints(): array + { + if ($this->throwPoints !== null) { + return $this->throwPoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->throwPoints = $this->variant->getThrowPoints(); + } + + $returnType = $this->variant->getReturnType(); + $throwType = $this->function->getThrowType(); + if ($throwType === null) { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + $throwPoints = []; + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + return $this->throwPoints = $throwPoints; + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + if ($this->impurePoints !== null) { + return $this->impurePoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->impurePoints = $this->variant->getImpurePoints(); + } + + $impurePoint = SimpleImpurePoint::createFromVariant($this->function, $this->variant); + if ($impurePoint === null) { + return $this->impurePoints = []; + } + + return $this->impurePoints = [$impurePoint]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->function->acceptsNamedArguments(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->function->mustUseReturnValue(); + } + +} 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 a89d59d7cc..6d0452caff 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -2,150 +2,28 @@ namespace PHPStan\Reflection; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\Php\PhpVersion; -use PHPStan\TrinaryLogic; -use PHPStan\Type\ConstantTypeHelper; +use PhpParser\Node\Expr; use PHPStan\Type\Type; -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 ?Type $phpDocType; + public function hasPhpDocType(): bool; - private PhpVersion $phpVersion; + public function getPhpDocType(): ?Type; - private ?string $deprecatedDescription; + public function hasNativeType(): bool; - private bool $isDeprecated; - - private bool $isInternal; - - private ?Type $valueType = null; - - public function __construct( - ClassReflection $declaringClass, - \ReflectionClassConstant $reflection, - ?Type $phpDocType, - PhpVersion $phpVersion, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal - ) - { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->phpDocType = $phpDocType; - $this->phpVersion = $phpVersion; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - } - - public function getName(): string - { - return $this->reflection->getName(); - } - - public function getFileName(): ?string - { - return $this->declaringClass->getFileName(); - } + public function getNativeType(): ?Type; /** - * @return mixed + * @return list */ - public function getValue() - { - try { - return $this->reflection->getValue(); - } catch (UnableToCompileNode $e) { - return NAN; - } - } - - public function hasPhpDocType(): bool - { - return $this->phpDocType !== null; - } - - public function getValueType(): Type - { - if ($this->valueType === null) { - if ($this->phpDocType === null) { - $this->valueType = ConstantTypeHelper::getTypeFromValue($this->getValue()); - } else { - $this->valueType = $this->phpDocType; - } - } - - return $this->valueType; - } - - public function getDeclaringClass(): ClassReflection - { - return $this->declaringClass; - } - - public function isStatic(): bool - { - return true; - } - - public function isPrivate(): bool - { - return $this->reflection->isPrivate(); - } - - public function isPublic(): bool - { - return $this->reflection->isPublic(); - } - - public function isFinal(): bool - { - if (method_exists($this->reflection, 'isFinal')) { - return $this->reflection->isFinal(); - } - - if (!$this->phpVersion->isInterfaceConstantImplicitlyFinal()) { - return false; - } - - return $this->declaringClass->isInterface(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isDeprecated); - } - - 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 b30294e758..9eeb979821 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -6,14 +6,24 @@ interface ClassMemberAccessAnswerer { + /** + * @phpstan-assert-if-true !null $this->getClassReflection() + */ public function isInClass(): bool; public function getClassReflection(): ?ClassReflection; + /** + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool; + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool; + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool; + public function canCallMethod(MethodReflection $methodReflection): bool; - public function canAccessConstant(ConstantReflection $constantReflection): bool; + public function canAccessConstant(ClassConstantReflection $constantReflection): bool; } diff --git a/src/Reflection/ClassNameHelper.php b/src/Reflection/ClassNameHelper.php index 803129f14a..815534f310 100644 --- a/src/Reflection/ClassNameHelper.php +++ b/src/Reflection/ClassNameHelper.php @@ -3,14 +3,23 @@ namespace PHPStan\Reflection; use Nette\Utils\Strings; +use function array_key_exists; +use function ltrim; -class ClassNameHelper +final class ClassNameHelper { + /** @var array */ + private static array $checked = []; + public static function isValidClassName(string $name): bool { - // from https://stackoverflow.com/questions/3195614/validate-class-method-names-with-regex#comment104531582_12011255 - return Strings::match(ltrim($name, '\\'), '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/') !== null; + if (!array_key_exists($name, self::$checked)) { + // from https://stackoverflow.com/questions/3195614/validate-class-method-names-with-regex#comment104531582_12011255 + self::$checked[$name] = Strings::match(ltrim($name, '\\'), '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/') !== null; + + } + return self::$checked[$name]; } } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 80b1cb4c09..a6404b7253 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -3,7 +3,16 @@ namespace PHPStan\Reflection; use Attribute; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; +use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\OutOfClassScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -13,11 +22,22 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; +use PHPStan\PhpDoc\Tag\SealedTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; use PHPStan\PhpDoc\Tag\TypeAliasTag; +use PHPStan\Reflection\Deprecation\DeprecationProvider; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\CircularTypeAliasDefinitionException; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; @@ -25,46 +45,57 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; -use ReflectionMethod; - -/** @api */ -class ClassReflection implements ReflectionWithFilename +use ReflectionException; +use function array_diff; +use function array_filter; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_shift; +use function array_unique; +use function array_values; +use function count; +use function implode; +use function in_array; +use function is_bool; +use function is_file; +use function is_int; +use function reset; +use function sprintf; +use function strtolower; + +/** + * @api + */ +final class ClassReflection { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private StubPhpDocProvider $stubPhpDocProvider; - - private PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private PhpVersion $phpVersion; - - /** @var \PHPStan\Reflection\PropertiesClassReflectionExtension[] */ - private array $propertiesClassReflectionExtensions; - - /** @var \PHPStan\Reflection\MethodsClassReflectionExtension[] */ - private array $methodsClassReflectionExtensions; - - private string $displayName; - - private \ReflectionClass $reflection; - - private ?string $anonymousFilename; - - /** @var \PHPStan\Reflection\MethodReflection[] */ + /** @var ExtendedMethodReflection[] */ private array $methods = []; - /** @var \PHPStan\Reflection\PropertyReflection[] */ + /** @var ExtendedPropertyReflection[] */ private array $properties = []; - /** @var \PHPStan\Reflection\ConstantReflection[] */ + /** @var ExtendedPropertyReflection[] */ + private array $instanceProperties = []; + + /** @var ExtendedPropertyReflection[] */ + private array $staticProperties = []; + + /** @var RealClassClassConstantReflection[] */ private array $constants = []; + /** @var EnumCaseReflection[]|null */ + private ?array $enumCases = null; + /** @var int[]|null */ private ?array $classHierarchyDistances = null; @@ -78,15 +109,19 @@ class ClassReflection implements ReflectionWithFilename private ?bool $isFinal = null; - /** @var ?TemplateTypeMap */ + private ?bool $isImmutable = null; + + private ?bool $hasConsistentConstructor = null; + + private ?bool $acceptsNamedArguments = null; + private ?TemplateTypeMap $templateTypeMap = null; - /** @var ?TemplateTypeMap */ - private ?TemplateTypeMap $resolvedTemplateTypeMap; + private ?TemplateTypeMap $activeTemplateTypeMap = null; - private ?ResolvedPhpDocBlock $stubPhpDocBlock; + private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null; - private ?string $extraCacheKey; + private ?TemplateTypeVarianceMap $callSiteVarianceMap = null; /** @var array|null */ private ?array $ancestors = null; @@ -96,17 +131,18 @@ class ClassReflection implements ReflectionWithFilename /** @var array */ private array $subclasses = []; - /** @var string|false|null */ - private $filename = false; + private string|false|null $filename = false; + + private string|false|null $reflectionDocComment = false; - /** @var string|false|null */ - private $reflectionDocComment = false; + private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; - /** @var \PHPStan\Reflection\ClassReflection[]|null */ + private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false; + + /** @var array|null */ private ?array $cachedInterfaces = null; - /** @var \PHPStan\Reflection\ClassReflection|false|null */ - private $cachedParentClass = false; + private ClassReflection|false|null $cachedParentClass = false; /** @var array|null */ private ?array $typeAliases = null; @@ -114,56 +150,61 @@ class ClassReflection implements ReflectionWithFilename /** @var array */ private static array $resolvingTypeAliasImports = []; + /** @var array */ + private array $hasMethodCache = []; + + /** @var array */ + private array $hasPropertyCache = []; + + /** @var array */ + private array $hasInstancePropertyCache = []; + + /** @var array */ + private array $hasStaticPropertyCache = []; + /** - * @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, - StubPhpDocProvider $stubPhpDocProvider, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - PhpVersion $phpVersion, - 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 PhpClassReflectionExtension $phpClassReflectionExtension, + 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->stubPhpDocProvider = $stubPhpDocProvider; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->phpVersion = $phpVersion; - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; - $this->displayName = $displayName; - $this->reflection = $reflection; - $this->anonymousFilename = $anonymousFilename; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - $this->stubPhpDocBlock = $stubPhpDocBlock; - $this->extraCacheKey = $extraCacheKey; } - public function getNativeReflection(): \ReflectionClass + public function getNativeReflection(): ReflectionClass|ReflectionEnum { return $this->reflection; } public function getFileName(): ?string { - if ($this->filename !== false) { + if (!is_bool($this->filename)) { return $this->filename; } @@ -175,25 +216,16 @@ public function getFileName(): ?string return $this->filename = null; } - if (!file_exists($fileName)) { + if (!is_file($fileName)) { return $this->filename = null; } return $this->filename = $fileName; } - public function getFileNameWithPhpDocs(): ?string - { - if ($this->stubPhpDocBlock !== null) { - return $this->stubPhpDocBlock->getFilename(); - } - - return $this->getFileName(); - } - public function getParentClass(): ?ClassReflection { - if ($this->cachedParentClass !== false) { + if (!is_bool($this->cachedParentClass)) { return $this->cachedParentClass; } @@ -211,7 +243,9 @@ public function getParentClass(): ?ClassReflection if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getActiveTemplateTypeMap() + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } @@ -225,7 +259,7 @@ public function getParentClass(): ?ClassReflection $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()), ); } @@ -244,19 +278,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 @@ -269,9 +310,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) { @@ -335,10 +389,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(); @@ -361,66 +414,236 @@ private function collectTraits(\ReflectionClass $class): array return $traits; } + public function allowsDynamicProperties(): bool + { + if ($this->isEnum()) { + return false; + } + + if (!$this->phpVersion->deprecatesDynamicProperties()) { + 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 || !$reflection->allowsDynamicProperties()) { + continue; + } + + return true; + } + + if ($this->isReadOnly()) { + return false; + } + + if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $this->reflectionProvider, + $this, + )) { + return true; + } + + $class = $this; + do { + $attributes = $class->reflection->getAttributes('AllowDynamicProperties'); + $class = $class->getParentClass(); + } while ($attributes === [] && $class !== null); + + return $attributes !== []; + } + + /** + * @deprecated Use hasInstanceProperty or hasStaticProperty instead + */ public function hasProperty(string $propertyName): bool { - foreach ($this->propertiesClassReflectionExtensions as $extension) { - if ($extension->hasProperty($this, $propertyName)) { - return true; + if (array_key_exists($propertyName, $this->hasPropertyCache)) { + return $this->hasPropertyCache[$propertyName]; + } + + if ($this->isEnum()) { + return $this->hasPropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); + } + + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + + if ($this->allowsDynamicProperties()) { + foreach ($this->propertiesClassReflectionExtensions as $extension) { + if ($extension->hasProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + } + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + + return $this->hasPropertyCache[$propertyName] = false; + } + + public function hasInstanceProperty(string $propertyName): bool + { + if (array_key_exists($propertyName, $this->hasInstancePropertyCache)) { + return $this->hasInstancePropertyCache[$propertyName]; + } + + if ($this->isEnum()) { + return $this->hasInstancePropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); + } + + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->phpClassReflectionExtension->getNativeProperty($this, $propertyName); + if (!$property->isStatic()) { + return $this->hasInstancePropertyCache[$propertyName] = true; + } + } + + if ($this->allowsDynamicProperties()) { + foreach ($this->propertiesClassReflectionExtensions as $extension) { + if ($extension->hasProperty($this, $propertyName)) { + $property = $extension->getProperty($this, $propertyName); + if ($property->isStatic()) { + continue; + } + return $this->hasInstancePropertyCache[$propertyName] = true; + } } } - return false; + if ($this->requireExtendsPropertiesClassReflectionExtension->hasInstanceProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + + return $this->hasPropertyCache[$propertyName] = false; + } + + public function hasStaticProperty(string $propertyName): bool + { + if (array_key_exists($propertyName, $this->hasStaticPropertyCache)) { + return $this->hasStaticPropertyCache[$propertyName]; + } + + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->phpClassReflectionExtension->getNativeProperty($this, $propertyName); + if ($property->isStatic()) { + return $this->hasStaticPropertyCache[$propertyName] = true; + } + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + return $this->hasStaticPropertyCache[$propertyName] = true; + } + + return $this->hasStaticPropertyCache[$propertyName] = false; } public function hasMethod(string $methodName): bool { + if (array_key_exists($methodName, $this->hasMethodCache)) { + return $this->hasMethodCache[$methodName]; + } + + if ($this->phpClassReflectionExtension->hasMethod($this, $methodName)) { + return $this->hasMethodCache[$methodName] = true; + } + 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); - if ($scope->canCallMethod($method)) { - return $this->methods[$key] = $method; - } + if ($this->phpClassReflectionExtension->hasMethod($this, $methodName)) { + $method = $this->phpClassReflectionExtension->getMethod($this, $methodName); + if ($scope->canCallMethod($method)) { + return $this->methods[$key] = $method; + } + $this->methods[$key] = $method; + } + + foreach ($this->methodsClassReflectionExtensions as $extension) { + if (!$extension->hasMethod($this, $methodName)) { + continue; + } + + $method = $this->wrapExtendedMethod($extension->getMethod($this, $methodName)); + if ($scope->canCallMethod($method)) { + return $this->methods[$key] = $method; + } + $this->methods[$key] = $method; + } + + if (!isset($this->methods[$key])) { + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + $method = $this->requireExtendsMethodsClassReflectionExtension->getMethod($this, $methodName); $this->methods[$key] = $method; } } if (!isset($this->methods[$key])) { - throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName); + 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); + return $this->phpClassReflectionExtension->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 $this->phpClassReflectionExtension->getNativeMethod($this, $methodName); } public function hasConstructor(): bool @@ -428,11 +651,11 @@ public function hasConstructor(): bool return $this->findConstructor() !== null; } - public function getConstructor(): MethodReflection + public function getConstructor(): ExtendedMethodReflection { $constructor = $this->findConstructor(); if ($constructor === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->getNativeMethod($constructor->getName()); } @@ -455,55 +678,197 @@ private function findConstructor(): ?ReflectionMethod return $constructor; } - private function getPhpExtension(): PhpClassReflectionExtension + /** @internal */ + public function evictPrivateSymbols(): void { - $extension = $this->methodsClassReflectionExtensions[0]; - if (!$extension instanceof PhpClassReflectionExtension) { - throw new \PHPStan\ShouldNotHappenException(); + 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->instanceProperties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + + unset($this->instanceProperties[$name]); } + foreach ($this->staticProperties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + + unset($this->staticProperties[$name]); + } + foreach ($this->methods as $name => $method) { + if (!$method->isPrivate()) { + continue; + } - return $extension; + unset($this->methods[$name]); + } + $this->phpClassReflectionExtension->evictPrivateSymbols($this->getCacheKey()); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + /** @deprecated Use getInstanceProperty or getStaticProperty */ + 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) { - if (!$extension->hasProperty($this, $propertyName)) { - continue; + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->phpClassReflectionExtension->getProperty($this, $propertyName, $scope); + if ($scope->canReadProperty($property)) { + return $this->properties[$key] = $property; } + $this->properties[$key] = $property; + } - $property = $extension->getProperty($this, $propertyName); - if ($scope->canAccessProperty($property)) { - return $this->properties[$key] = $property; + if ($this->allowsDynamicProperties()) { + foreach ($this->propertiesClassReflectionExtensions as $extension) { + if (!$extension->hasProperty($this, $propertyName)) { + continue; + } + + $property = $this->wrapExtendedProperty($propertyName, $extension->getProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { + return $this->properties[$key] = $property; + } + $this->properties[$key] = $property; } + } + } + + // For BC purpose + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->phpClassReflectionExtension->getProperty($this, $propertyName, $scope); + + return $this->properties[$key] = $property; + } + + if (!isset($this->properties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getProperty($this, $propertyName); $this->properties[$key] = $property; } } if (!isset($this->properties[$key])) { - throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName); + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } return $this->properties[$key]; } + public function getInstanceProperty(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->instanceProperties[$key])) { + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->phpClassReflectionExtension->getProperty($this, $propertyName, $scope); + if (!$property->isStatic()) { + if ($scope->canReadProperty($property)) { + return $this->instanceProperties[$key] = $property; + } + $this->instanceProperties[$key] = $property; + } + } + + if ($this->allowsDynamicProperties()) { + foreach ($this->propertiesClassReflectionExtensions as $extension) { + if (!$extension->hasProperty($this, $propertyName)) { + continue; + } + + $nakedProperty = $extension->getProperty($this, $propertyName); + if ($nakedProperty->isStatic()) { + continue; + } + + $property = $this->wrapExtendedProperty($propertyName, $nakedProperty); + if ($scope->canReadProperty($property)) { + return $this->instanceProperties[$key] = $property; + } + $this->instanceProperties[$key] = $property; + } + } + } + + if (!isset($this->instanceProperties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasInstanceProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getInstanceProperty($this, $propertyName); + $this->instanceProperties[$key] = $property; + } + } + + if (!isset($this->instanceProperties[$key])) { + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + } + + return $this->instanceProperties[$key]; + } + + public function getStaticProperty(string $propertyName): ExtendedPropertyReflection + { + $key = $propertyName; + if (isset($this->staticProperties[$key])) { + return $this->staticProperties[$key]; + } + + if ($this->phpClassReflectionExtension->hasProperty($this, $propertyName)) { + $nakedProperty = $this->phpClassReflectionExtension->getProperty($this, $propertyName, new OutOfClassScope()); + if ($nakedProperty->isStatic()) { + $property = $this->wrapExtendedProperty($propertyName, $nakedProperty); + if ($property->isStatic()) { + return $this->staticProperties[$key] = $property; + } + } + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getStaticProperty($this, $propertyName); + return $this->staticProperties[$key] = $property; + } + + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + } + public function hasNativeProperty(string $propertyName): bool { - return $this->getPhpExtension()->hasProperty($this, $propertyName); + return $this->phpClassReflectionExtension->hasProperty($this, $propertyName); } 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); + return $this->phpClassReflectionExtension->getNativeProperty($this, $propertyName); } public function isAbstract(): bool @@ -516,14 +881,128 @@ public function isInterface(): bool return $this->reflection->isInterface(); } - public function isTrait(): bool - { - return $this->reflection->isTrait(); + public function isTrait(): bool + { + return $this->reflection->isTrait(); + } + + /** + * @phpstan-assert-if-true ReflectionEnum $this->reflection + * @phpstan-assert-if-true ReflectionEnum $this->getNativeReflection() + */ + public function isEnum(): bool + { + return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); + } + + /** + * @return 'Interface'|'Trait'|'Enum'|'Class' + */ + public function getClassTypeDescription(): string + { + if ($this->isInterface()) { + return 'Interface'; + } elseif ($this->isTrait()) { + return 'Trait'; + } elseif ($this->isEnum()) { + return 'Enum'; + } + + return 'Class'; + } + + public function isReadOnly(): bool + { + return $this->reflection->isReadOnly(); + } + + public function isBackedEnum(): bool + { + if (!$this->reflection instanceof ReflectionEnum) { + return false; + } + + return $this->reflection->isBacked(); + } + + public function getBackedEnumType(): ?Type + { + if (!$this->reflection instanceof ReflectionEnum) { + return null; + } + + if (!$this->reflection->isBacked()) { + return null; + } + + return TypehintHelper::decideTypeFromReflection($this->reflection->getBackingType()); + } + + public function hasEnumCase(string $name): bool + { + if (!$this->isEnum()) { + return false; + } + + return $this->reflection->hasCase($name); + } + + /** + * @return array + */ + public function getEnumCases(): array + { + if (!$this->isEnum()) { + throw new ShouldNotHappenException(); + } + + if ($this->enumCases !== null) { + return $this->enumCases; + } + + $cases = []; + $initializerExprContext = InitializerExprContext::fromClassReflection($this); + foreach ($this->reflection->getCases() as $case) { + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = $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); + } + + return $this->enumCases = $cases; + } + + public function getEnumCase(string $name): EnumCaseReflection + { + if (!$this->hasEnumCase($name)) { + throw new ShouldNotHappenException(sprintf('Enum case %s::%s does not exist.', $this->getDisplayName(), $name)); + } + + if (!$this->reflection instanceof ReflectionEnum) { + throw new ShouldNotHappenException(); + } + + if ($this->enumCases !== null && array_key_exists($name, $this->enumCases)) { + return $this->enumCases[$name]; + } + + $case = $this->reflection->getCase($name); + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); + } + + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + + return new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); } public function isClass(): bool { - return !$this->isInterface() && !$this->isTrait(); + return !$this->isInterface() && !$this->isTrait() && !$this->isEnum(); } public function isAnonymous(): bool @@ -531,20 +1010,38 @@ public function isAnonymous(): bool return $this->anonymousFilename !== null; } + public function is(string $className): bool + { + return $this->getName() === $className || $this->isSubclassOf($className); + } + + /** + * @deprecated Use isSubclassOfClass instead. + */ public function isSubclassOf(string $className): bool { - if (isset($this->subclasses[$className])) { - return $this->subclasses[$className]; + if (!$this->reflectionProvider->hasClass($className)) { + return false; } - if (!$this->reflectionProvider->hasClass($className)) { - return $this->subclasses[$className] = false; + return $this->isSubclassOfClass($this->reflectionProvider->getClass($className)); + } + + public function isSubclassOfClass(self $class): bool + { + $cacheKey = $class->getCacheKey(); + if (isset($this->subclasses[$cacheKey])) { + return $this->subclasses[$cacheKey]; + } + + if ($class->isFinalByKeyword() || $class->isAnonymous()) { + return $this->subclasses[$cacheKey] = false; } try { - return $this->subclasses[$className] = $this->reflection->isSubclassOf($className); - } catch (\ReflectionException $e) { - return $this->subclasses[$className] = false; + return $this->subclasses[$cacheKey] = $this->reflection->isSubclassOf($class->getName()); + } catch (ReflectionException) { + return $this->subclasses[$cacheKey] = false; } } @@ -552,13 +1049,13 @@ public function implementsInterface(string $className): bool { try { return $this->reflection->implementsInterface($className); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return false; } } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return list */ public function getParents(): array { @@ -573,7 +1070,7 @@ public function getParents(): array } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return array */ public function getInterfaces(): array { @@ -607,7 +1104,7 @@ public function getInterfaces(): array } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return array */ private function collectInterfaces(ClassReflection $interface): array { @@ -623,7 +1120,7 @@ private function collectInterfaces(ClassReflection $interface): array } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return array */ public function getImmediateInterfaces(): array { @@ -663,7 +1160,10 @@ public function getImmediateInterfaces(): array if ($this->isGeneric()) { $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, - $this->getActiveTemplateTypeMap() + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), + true, ); } @@ -678,7 +1178,7 @@ public function getImmediateInterfaces(): array if ($immediateInterface->isGeneric()) { $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface->withTypes( - array_values($immediateInterface->getTemplateTypeMap()->resolveToBounds()->getTypes()) + array_values($immediateInterface->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), ); continue; } @@ -690,7 +1190,7 @@ public function getImmediateInterfaces(): array } /** - * @return array + * @return array */ public function getTraits(bool $recursive = false): array { @@ -704,9 +1204,7 @@ public function getTraits(bool $recursive = false): array $traits = $this->getNativeReflection()->getTraits(); } - $traits = array_map(function (\ReflectionClass $trait): ClassReflection { - return $this->reflectionProvider->getClass($trait->getName()); - }, $traits); + $traits = array_map(fn (ReflectionClass $trait): ClassReflection => $this->reflectionProvider->getClass($trait->getName()), $traits); if ($recursive) { $parentClass = $this->getNativeReflection()->getParentClass(); @@ -714,7 +1212,7 @@ public function getTraits(bool $recursive = false): array if ($parentClass !== false) { return array_merge( $traits, - $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true) + $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true), ); } } @@ -723,15 +1221,15 @@ public function getTraits(bool $recursive = false): array } /** - * @return string[] + * @return list */ public function getParentClassesNames(): array { $parentNames = []; - $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== null) { - $parentNames[] = $currentClassReflection->getParentClass()->getName(); - $currentClassReflection = $currentClassReflection->getParentClass(); + $parentClass = $this->getParentClass(); + while ($parentClass !== null) { + $parentNames[] = $parentClass->getName(); + $parentClass = $parentClass->getParentClass(); } return $parentNames; @@ -751,25 +1249,26 @@ public function hasConstant(string $name): bool return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); } - public function getConstant(string $name): ConstantReflection + public function getConstant(string $name): ClassConstantReflection { if (!isset($this->constants[$name])) { $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); if ($reflectionConstant === false) { - throw new \PHPStan\Reflection\MissingConstantFromReflectionException($this->getName(), $name); + throw new MissingConstantFromReflectionException($this->getName(), $name); } - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; + $deprecation = $this->deprecationProvider->getClassConstantDeprecation($reflectionConstant); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()); $fileName = $declaringClass->getFileName(); $phpDocType = null; $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( $declaringClass->getName(), - $name + $name, ); - if ($resolvedPhpDoc === null && $fileName !== null) { + if ($resolvedPhpDoc === null) { $docComment = null; if ($reflectionConstant->getDocComment() !== false) { $docComment = $reflectionConstant->getDocComment(); @@ -778,28 +1277,43 @@ public function getConstant(string $name): ConstantReflection $docComment, $declaringClass, $fileName, - $name + $name, ); } - if ($resolvedPhpDoc !== null) { + if (!$isDeprecated) { $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - $varTags = $resolvedPhpDoc->getVarTags(); - if (isset($varTags[0]) && count($varTags) === 1) { - $phpDocType = $varTags[0]->getType(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $declaringClass); + } elseif ($this->signatureMapProvider->hasClassConstantMetadata($declaringClass->getName(), $name)) { + $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; + } + + $varTags = $resolvedPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $varTag = $varTags[0]; + if ($varTag->isExplicit() || $nativeType === null || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { + $phpDocType = $varTag->getType(); } } - $this->constants[$name] = new ClassConstantReflection( + $this->constants[$name] = new RealClassClassConstantReflection( + $this->initializerExprTypeResolver, $declaringClass, $reflectionConstant, + $nativeType, $phpDocType, - $this->phpVersion, $deprecatedDescription, $isDeprecated, - $isInternal + $isInternal, + $isFinal, + $this->attributeReflectionFactory->fromNativeReflection($reflectionConstant->getAttributes(), InitializerExprContext::fromClass($declaringClass->getName(), $fileName)), ); } return $this->constants[$name]; @@ -811,12 +1325,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(); @@ -841,7 +1355,7 @@ public function getTypeAliases(): array // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { - throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); + throw new CircularTypeAliasDefinitionException(); } self::$resolvingTypeAliasImports[$this->getName()] = true; @@ -858,7 +1372,7 @@ public function getTypeAliases(): array try { $typeAliases = $importedFromReflection->getTypeAliases(); - } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { + } catch (CircularTypeAliasDefinitionException) { return TypeAlias::invalid(); } @@ -871,15 +1385,11 @@ public function getTypeAliases(): array unset(self::$resolvingTypeAliasImports[$this->getName()]); - $localAliases = array_map(static function (TypeAliasTag $typeAliasTag): TypeAlias { - return $typeAliasTag->getTypeAlias(); - }, $typeAliasTags); + $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); $this->typeAliases = array_filter( array_merge($importedAliases, $localAliases), - static function (?TypeAlias $typeAlias): bool { - return $typeAlias !== null; - } + static fn (?TypeAlias $typeAlias): bool => $typeAlias !== null, ); } @@ -888,11 +1398,8 @@ static function (?TypeAlias $typeAlias): bool { public function getDeprecatedDescription(): ?string { - if ($this->deprecatedDescription === null && $this->isDeprecated()) { - $resolvedPhpDoc = $this->getResolvedPhpDoc(); - if ($resolvedPhpDoc !== null && $resolvedPhpDoc->getDeprecatedTag() !== null) { - $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); - } + if ($this->isDeprecated === null) { + $this->resolveDeprecation(); } return $this->deprecatedDescription; @@ -901,13 +1408,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(); @@ -925,63 +1455,156 @@ 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->findAttributeClass() !== null; + return $this->findAttributeFlags() !== null; } - private function findAttributeClass(): ?Attribute + private function findAttributeFlags(): ?int { - if ($this->isInterface() || $this->isTrait()) { + if ($this->isInterface() || $this->isTrait() || $this->isEnum()) { return null; } - if ($this->reflection instanceof ReflectionClass) { - foreach ($this->reflection->getBetterReflection()->getAttributes() as $attribute) { - if ($attribute->getName() === \Attribute::class) { - /** @var \Attribute */ - return $attribute->newInstance(); + $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, name: is_int($i) ? null : new Identifier($i)); } - return null; - } + 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 (!method_exists($this->reflection, 'getAttributes')) { - return null; - } + if (!$flagType instanceof ConstantIntegerType) { + return null; + } - $nativeAttributes = $this->reflection->getAttributes(\Attribute::class); - if (count($nativeAttributes) === 1) { - /** @var Attribute */ - return $nativeAttributes[0]->newInstance(); + return $flagType->getValue(); } return null; } + /** + * @return list + */ + public function getAttributes(): array + { + return $this->attributeReflectionFactory->fromNativeReflection($this->reflection->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + } + public function getAttributeClassFlags(): int { - $attribute = $this->findAttributeClass(); - if ($attribute === null) { - throw new \PHPStan\ShouldNotHappenException(); + $flags = $this->findAttributeFlags(); + if ($flags === null) { + throw new ShouldNotHappenException(); + } + + return $flags; + } + + public function getObjectType(): ObjectType + { + if (!$this->isGeneric()) { + return new ObjectType($this->getName()); } - return $attribute->flags; + return new GenericObjectType( + $this->getName(), + $this->typeMapToList($this->getActiveTemplateTypeMap()), + variances: $this->varianceMapToList($this->getCallSiteVarianceMap()), + ); } public function getTemplateTypeMap(): TemplateTypeMap @@ -998,9 +1621,7 @@ public function getTemplateTypeMap(): TemplateTypeMap $templateTypeScope = TemplateTypeScope::createWithClass($this->getName()); - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $this->getTemplateTags())); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $this->getTemplateTags())); $this->templateTypeMap = $templateTypeMap; @@ -1008,13 +1629,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; } @@ -1023,7 +1697,6 @@ public function isGeneric(): bool /** * @param array $types - * @return \PHPStan\Type\Generic\TemplateTypeMap */ public function typeMapFromList(array $types): TemplateTypeMap { @@ -1035,14 +1708,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(); @@ -1052,7 +1745,23 @@ public function typeMapToList(TemplateTypeMap $typeMap): array $list = []; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound(); + $list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound(); + } + + return $list; + } + + /** @return list */ + public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + $list = []; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant(); } return $list; @@ -1065,17 +1774,148 @@ 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->phpClassReflectionExtension, $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, + ); + } + + /** + * @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->phpClassReflectionExtension, + $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->phpClassReflectionExtension, + $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 removeFinalKeywordOverride(): self + { + if ($this->getNativeReflection()->isFinal()) { + return $this; + } + if ($this->finalByKeywordOverride === false) { + 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->phpClassReflectionExtension, + $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, + false, ); } @@ -1086,11 +1926,32 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock } $fileName = $this->getFileName(); - if ($fileName === null) { + if (is_bool($this->reflectionDocComment)) { + $docComment = $this->reflection->getDocComment(); + $this->reflectionDocComment = $docComment !== false ? $docComment : null; + } + + if ($this->reflectionDocComment === null) { return null; } - if ($this->reflectionDocComment === 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; } @@ -1099,7 +1960,11 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return null; } - return $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + if ($this->traitContextResolvedPhpDocBlock !== false) { + return $this->traitContextResolvedPhpDocBlock; + } + + return $this->traitContextResolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $implementingClass->getName(), $this->getName(), null, $this->reflectionDocComment); } private function getFirstExtendsTag(): ?ExtendsTag @@ -1228,7 +2093,46 @@ 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 getSealedTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getSealedTags(); + } + + /** + * @return array */ public function getPropertyTags(): array { @@ -1241,7 +2145,7 @@ public function getPropertyTags(): array } /** - * @return array + * @return array */ public function getMethodTags(): array { @@ -1254,7 +2158,7 @@ public function getMethodTags(): array } /** - * @return array + * @return list */ public function getResolvedMixinTypes(): array { @@ -1267,11 +2171,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..dd2a7d7b69 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -2,41 +2,31 @@ namespace PHPStan\Reflection; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +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, + private PhpClassReflectionExtension $phpClassReflectionExtension, ) { - 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 +34,34 @@ 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; + } + + public function getPhpClassReflectionExtension(): PhpClassReflectionExtension + { + return $this->phpClassReflectionExtension; + } + } 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 186da6e891..ebae755849 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -2,13 +2,23 @@ namespace PHPStan\Reflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; + /** @api */ -interface ConstantReflection extends ClassMemberReflection, GlobalConstantReflection +interface ConstantReflection { - /** - * @return mixed - */ - public function getValue(); + public function getName(): string; + + public function getValueType(): Type; + + public function isDeprecated(): TrinaryLogic; + + public function getDeprecatedDescription(): ?string; + + public function isInternal(): TrinaryLogic; + + public function getFileName(): ?string; } diff --git a/src/Reflection/ConstructorsHelper.php b/src/Reflection/ConstructorsHelper.php new file mode 100644 index 0000000000..fc38d72298 --- /dev/null +++ b/src/Reflection/ConstructorsHelper.php @@ -0,0 +1,81 @@ +> */ + private array $additionalConstructorsCache = []; + + /** + * @param list $additionalConstructors + */ + public function __construct( + private Container $container, + #[AutowiredParameter] + 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..ebd25cde0d --- /dev/null +++ b/src/Reflection/Deprecation/DeprecationProvider.php @@ -0,0 +1,146 @@ + $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(ClassReflection $declaringClass, MethodReflection $reflection, array $variants) + public function __construct( + private ClassReflection $declaringClass, + private ExtendedMethodReflection $reflection, + private array $variants, + private ?array $namedArgumentsVariants, + private ?Type $selfOutType, + private ?Type $throwType, + private Assertions $assertions, + ) { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->variants = $variants; } public function getDeclaringClass(): ClassReflection @@ -70,6 +72,21 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function isDeprecated(): TrinaryLogic { return $this->reflection->isDeprecated(); @@ -85,14 +102,29 @@ 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(); + return $this->throwType; } public function hasSideEffects(): TrinaryLogic @@ -100,4 +132,49 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function getAsserts(): Assertions + { + return $this->assertions; + } + + 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->reflection->mustUseReturnValue(); + } + } diff --git a/src/Reflection/Dummy/ChangedTypePropertyReflection.php b/src/Reflection/Dummy/ChangedTypePropertyReflection.php index e8217e91f4..c96fdd7f04 100644 --- a/src/Reflection/Dummy/ChangedTypePropertyReflection.php +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -3,28 +3,22 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class ChangedTypePropertyReflection implements WrapperPropertyReflection +final class ChangedTypePropertyReflection implements WrapperPropertyReflection { - private ClassReflection $declaringClass; - - private PropertyReflection $reflection; - - private Type $readableType; - - private Type $writableType; + public function __construct(private ClassReflection $declaringClass, private ExtendedPropertyReflection $reflection, private Type $readableType, private Type $writableType, private Type $phpDocType, private Type $nativeType) + { + } - public function __construct(ClassReflection $declaringClass, PropertyReflection $reflection, Type $readableType, Type $writableType) + public function getName(): string { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->readableType = $readableType; - $this->writableType = $writableType; + return $this->reflection->getName(); } public function getDeclaringClass(): ClassReflection @@ -52,6 +46,26 @@ public function getDocComment(): ?string return $this->reflection->getDocComment(); } + public function hasPhpDocType(): bool + { + return $this->reflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return $this->reflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + public function getReadableType(): Type { return $this->readableType; @@ -92,9 +106,59 @@ public function isInternal(): TrinaryLogic return $this->reflection->isInternal(); } - public function getOriginalReflection(): PropertyReflection + public function getOriginalReflection(): ExtendedPropertyReflection { return $this->reflection; } + public function isAbstract(): TrinaryLogic + { + return $this->reflection->isAbstract(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + + 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(); + } + + public function isDummy(): TrinaryLogic + { + return $this->reflection->isDummy(); + } + } 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 f45c4797b4..0000000000 --- a/src/Reflection/Dummy/DummyConstantReflection.php +++ /dev/null @@ -1,88 +0,0 @@ -name = $name; - } - - public function getDeclaringClass(): ClassReflection - { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - - return $reflectionProvider->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..6652c87c47 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,50 @@ 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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 29e32c76e0..f81481d449 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,29 +2,29 @@ namespace PHPStan\Reflection\Dummy; +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 { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $reflectionProvider->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,45 @@ 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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Dummy/DummyPropertyReflection.php b/src/Reflection/Dummy/DummyPropertyReflection.php index 9ae5f17a5e..e39d1dbb6c 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -3,20 +3,32 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use stdClass; -class DummyPropertyReflection implements PropertyReflection +final class DummyPropertyReflection implements ExtendedPropertyReflection { + public function __construct(private string $name) + { + } + + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $reflectionProvider->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,54 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/EnumCaseReflection.php b/src/Reflection/EnumCaseReflection.php new file mode 100644 index 0000000000..94cced2739 --- /dev/null +++ b/src/Reflection/EnumCaseReflection.php @@ -0,0 +1,90 @@ + $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 getEnumCaseObjectType(): EnumCaseObjectType + { + return new EnumCaseObjectType( + $this->declaringEnum->getName(), + $this->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..4cac48123d --- /dev/null +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -0,0 +1,89 @@ + $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, + private TrinaryLogic $mustUseReturnValue, + ) + { + 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; + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; + } + +} 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..0cb9caeab0 --- /dev/null +++ b/src/Reflection/ExtendedMethodReflection.php @@ -0,0 +1,75 @@ + + */ + 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; + + /** + * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return + * value is unused at runtime a warning is emitted, PHPStan will emit the + * warning during analysis and on older PHP versions too + */ + public function mustUseReturnValue(): TrinaryLogic; + +} 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..16d0f895ea --- /dev/null +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -0,0 +1,74 @@ + + */ + public function getAttributes(): array; + + /** + * If property has been declared in code then this returns `no()` + * + * Returns `yes()` if the property represents possibly-defined property + * in non-final classes, on mixed, on object etc. + */ + public function isDummy(): TrinaryLogic; + +} diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 6520745366..b99209c628 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -11,17 +11,29 @@ 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; @@ -30,4 +42,31 @@ 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; + + /** + * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return + * value is unused at runtime a warning is emitted, PHPStan will emit the + * warning during analysis and on older PHP versions too + */ + public function mustUseReturnValue(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index dccd3a19b0..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,21 +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|null $filename - * @param bool|null $isPure - * @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, @@ -32,9 +26,15 @@ public function create( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, ?string $filename, - ?bool $isPure = null + ?bool $isPure, + Assertions $asserts, + bool $acceptsNamedArguments, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $phpDocParameterImmediatelyInvokedCallable, + array $phpDocParameterClosureThisTypes, + array $attributes, ): PhpFunctionReflection; } diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 9a354bbbb2..7c69274ef0 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -3,42 +3,31 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -/** @api */ +/** + * @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; /** * @api - * @param array $parameters - * @param bool $isVariadic - * @param Type $returnType + * @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 @@ -51,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 0a9554f394..0000000000 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ /dev/null @@ -1,67 +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/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 34c56baf84..784b1427d1 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -2,45 +2,137 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; +use PHPStan\TrinaryLogic; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_key_exists; +use function array_map; +use function array_merge; +use function count; +use function is_int; -class GenericParametersAcceptorResolver +final class GenericParametersAcceptorResolver { /** * @api - * @param \PHPStan\Type\Type[] $argTypes + * @param array $argTypes */ - public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor + public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ExtendedParametersAcceptor { $typeMap = TemplateTypeMap::createEmpty(); + $passedArgs = []; - foreach ($parametersAcceptor->getParameters() as $i => $param) { - if (isset($argTypes[$i])) { - $argType = $argTypes[$i]; + $parameters = $parametersAcceptor->getParameters(); + $namedArgTypes = []; + foreach ($argTypes as $i => $argType) { + if (is_int($i)) { + if (isset($parameters[$i])) { + $namedArgTypes[$parameters[$i]->getName()] = $argType; + continue; + } + if (count($parameters) > 0) { + $lastParameter = $parameters[count($parameters) - 1]; + if ($lastParameter->isVariadic()) { + $parameterName = $lastParameter->getName(); + if (array_key_exists($parameterName, $namedArgTypes)) { + $namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $argType); + continue; + } + $namedArgTypes[$parameterName] = $argType; + } + } + continue; + } + + $namedArgTypes[$i] = $argType; + } + + foreach ($parametersAcceptor->getParameters() as $param) { + if (isset($namedArgTypes[$param->getName()])) { + $argType = $namedArgTypes[$param->getName()]; } elseif ($param->getDefaultValue() !== null) { $argType = $param->getDefaultValue(); } else { - break; + continue; } $paramType = $param->getType(); + $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); + $passedArgs['$' . $param->getName()] = $argType; + } - // todo zde "doaplikovat" typy, ktere se dosud nevyskytly - typicky callable(T) - parametr callable + $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(), + $originalParametersAcceptor->mustUseReturnValue(), + ); + } + + return $result; } } diff --git a/src/Reflection/GlobalConstantReflection.php b/src/Reflection/GlobalConstantReflection.php deleted file mode 100644 index 6483e72e86..0000000000 --- a/src/Reflection/GlobalConstantReflection.php +++ /dev/null @@ -1,24 +0,0 @@ -methodReflection = $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,45 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } 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..8b38c5a383 --- /dev/null +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -0,0 +1,2233 @@ + */ + private array $currentlyResolvingClassConstant = []; + + public function __construct( + private ConstantResolver $constantResolver, + private ReflectionProviderProvider $reflectionProviderProvider, + private PhpVersion $phpVersion, + private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private OversizedArrayBuilder $oversizedArrayBuilder, + #[AutowiredParameter] + 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->hasInstanceProperty($expr->name->name)->yes()) { + return new ErrorType(); + } + + return $fetchedOnType->getInstanceProperty($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; + if (!$generalize) { + 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()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + 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; + if (!$generalize) { + 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()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + 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; + if (!$generalize) { + 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()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + 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; + if (!$generalize) { + 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 + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { + if (in_array($scalarValue, [0, 0.0], true)) { + 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->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->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; + if (!$generalize) { + 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); + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $integerType = $rightType->toInteger(); + if ($integerType instanceof ConstantIntegerType && $integerType->getValue() === 1) { + return new ConstantIntegerType(0); + } + + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { + + if (in_array($scalarValue, [0, 0.0], true)) { + 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; + if (!$generalize) { + 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()); + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $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()), + ); + + foreach ($leftType->getConstantArrays() as $type) { + foreach ($type->getKeyTypes() as $i => $offsetType) { + if ($type->isOptionalKey($i)) { + continue; + } + $valueType = $type->getValueTypes()[$i]; + $arrayType = TypeCombinator::intersect($arrayType, new HasOffsetValueType($offsetType, $valueType)); + } + } + + 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; + if (!$generalize) { + 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()); + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + 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; + if (!$generalize) { + 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()); + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $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->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->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; + if (!$generalize) { + 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())); + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $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; + if (!$generalize) { + 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())); + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); + } + + $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); + } + + private function optimizeScalarType(Type $type): Type + { + $types = []; + if ($type->isInteger()->yes()) { + $types[] = new IntegerType(); + } + if ($type->isString()->yes()) { + $types[] = new StringType(); + } + if ($type->isFloat()->yes()) { + $types[] = new FloatType(); + } + if ($type->isNull()->yes()) { + $types[] = new NullType(); + } + + if (count($types) === 0) { + return new ErrorType(); + } + + if (count($types) === 1) { + return $types[0]; + } + + return new UnionType($types); + } + + /** + * @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(), []); + } + + /** + * @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->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->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)) { + if ($constantClassReflection->getName() === 'Attribute' && $constantName === 'TARGET_CONSTANT') { + return new ConstantIntegerType(1 << 16); + } + 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(), selfClass: $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..b47b4930cb 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -2,55 +2,26 @@ namespace PHPStan\Reflection; -class MethodPrototypeReflection implements ClassMemberReflection -{ - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private string $name; - - private bool $isStatic; - - private bool $isPrivate; - - private bool $isPublic; +use PHPStan\Type\Type; - private bool $isAbstract; - - private bool $isFinal; - - /** @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 $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 @@ -83,9 +54,9 @@ public function isAbstract(): bool return $this->isAbstract; } - public function isFinal(): bool + public function isInternal(): bool { - return $this->isFinal; + return $this->isInternal; } public function getDocComment(): ?string @@ -101,4 +72,9 @@ public function getVariants(): array return $this->variants; } + public function getTentativeReturnType(): ?Type + { + return $this->tentativeReturnType; + } + } diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 18443d371b..529a5011dd 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -14,7 +14,7 @@ public function getName(): string; public function getPrototype(): ClassMemberReflection; /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return list */ public function getVariants(): array; diff --git a/src/Reflection/MethodsClassReflectionExtension.php b/src/Reflection/MethodsClassReflectionExtension.php index 6b12291b0b..5817ac7657 100644 --- a/src/Reflection/MethodsClassReflectionExtension.php +++ b/src/Reflection/MethodsClassReflectionExtension.php @@ -2,7 +2,23 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * This is the interface custom methods class reflection extensions implement. + * + * To register it in the configuration file use the `phpstan.broker.methodsClassReflectionExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyMethodsClassReflectionExtension + * tags: + * - phpstan.broker.methodsClassReflectionExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/class-reflection-extensions + * + * @api + */ interface MethodsClassReflectionExtension { diff --git a/src/Reflection/MissingConstantFromReflectionException.php b/src/Reflection/MissingConstantFromReflectionException.php index 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/MissingStaticAccessorInstanceException.php b/src/Reflection/MissingStaticAccessorInstanceException.php new file mode 100644 index 0000000000..94bf6a28e4 --- /dev/null +++ b/src/Reflection/MissingStaticAccessorInstanceException.php @@ -0,0 +1,10 @@ +reflection = $reflection; - $this->static = $static; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index 6c47734dcd..dd2b84938e 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,15 +43,26 @@ 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 @@ -61,13 +74,21 @@ private function findMethod(ClassReflection $classReflection, string $methodName return new MixinMethodReflection($method, $static); } - foreach ($classReflection->getParents() as $parentClass) { - $method = $this->findMethod($parentClass, $methodName); - if ($method === null) { + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findMethod($traitClass, $methodName); + if ($methodWithDeclaringClass === null) { continue; } - return $method; + return $methodWithDeclaringClass; + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { + $method = $this->findMethod($parentClass, $methodName); + if ($method !== null) { + return $method; + } } return null; diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 4ab4635111..b999c1cf61 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,26 +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; } - if (!$type->hasProperty($propertyName)->yes()) { + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + if (isset($this->inProcess[$typeDescription][$propertyName])) { continue; } - return $type->getProperty($propertyName, new OutOfClassScope()); - } + $this->inProcess[$typeDescription][$propertyName] = true; - foreach ($classReflection->getParents() as $parentClass) { - $property = $this->findProperty($parentClass, $propertyName); - if ($property === null) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { + unset($this->inProcess[$typeDescription][$propertyName]); continue; } + $property = $type->getInstanceProperty($propertyName, new OutOfClassScope()); + unset($this->inProcess[$typeDescription][$propertyName]); + return $property; } + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findProperty($traitClass, $propertyName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { + $property = $this->findProperty($parentClass, $propertyName); + if ($property !== null) { + return $property; + } + } + 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 901d61e443..50ddbb0e9b 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -2,39 +2,44 @@ 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 PHPStan\Type\VoidType; +use function count; +use function strtolower; -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 @@ -42,14 +47,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; @@ -62,7 +84,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->isDeprecated); } public function isInternal(): TrinaryLogic @@ -70,11 +92,6 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isFinal(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - public function hasSideEffects(): TrinaryLogic { if ($this->isVoid()) { @@ -84,10 +101,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -100,4 +126,39 @@ 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; + } + + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 1410a99583..7850ed1b5d 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -2,60 +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\VoidType; +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; - - private ?Type $throwType; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @param BuiltinMethodReflection $reflection - * @param \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] $variants - * @param TrinaryLogic $hasSideEffects - * @param string|null $stubPhpDocString - * @param Type|null $throwType + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflection $declaringClass, - BuiltinMethodReflection $reflection, - array $variants, - TrinaryLogic $hasSideEffects, - ?string $stubPhpDocString, - ?Type $throwType + 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; - $this->throwType = $throwType; } public function getDeclaringClass(): ClassReflection @@ -78,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(), selfClass: $prototypeDeclaringClass); + } return new MethodPrototypeReflection( $prototypeMethod->getName(), @@ -96,10 +94,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPrivate(), $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; } } @@ -109,14 +108,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; @@ -124,7 +135,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return $this->reflection->isDeprecated(); + return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); } public function isInternal(): TrinaryLogic @@ -132,11 +143,21 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + public function getThrowType(): ?Type { return $this->throwType; @@ -156,10 +177,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -169,11 +199,42 @@ private function isVoid(): bool public function getDocComment(): ?string { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } + return $this->phpDocComment; + } + + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments); + } - return $this->reflection->getDocComment(); + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); } } 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/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..fc9b44f79a --- /dev/null +++ b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === $this->methodName; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + return new ObjectType(ReflectionClass::class); + } + +} diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ParameterReflectionWithPhpDocs.php deleted file mode 100644 index e0ede3fd51..0000000000 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ /dev/null @@ -1,14 +0,0 @@ - + * @return list */ public function getParameters(): array; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 3b3db43e26..6b47480d73 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,48 +2,76 @@ namespace PHPStan\Reflection; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; +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\Parser\ImplodeArgVisitor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Php\ExtendedDummyParameter; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\IntegerType; +use PHPStan\Type\LateResolvableType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\ResourceType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_key_last; +use function array_map; +use function array_merge; +use function array_slice; +use function array_values; +use function constant; +use function count; +use function defined; +use function is_string; +use function sprintf; +use const ARRAY_FILTER_USE_BOTH; +use const ARRAY_FILTER_USE_KEY; +use const CURLOPT_SSL_VERIFYHOST; -/** @api */ -class ParametersAcceptorSelector +/** + * @api + */ +final class ParametersAcceptorSelector { /** - * @template T of ParametersAcceptor - * @param T[] $parametersAcceptors - * @return T - */ - public static function selectSingle( - array $parametersAcceptors - ): ParametersAcceptor - { - 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 = []; @@ -52,38 +80,37 @@ public static function selectFromArgs( count($args) > 0 && count($parametersAcceptors) > 0 ) { - $functionName = null; - $argParent = $args[0]->getAttribute('parent'); - if ($argParent instanceof FuncCall && $argParent->name instanceof Name) { - $functionName = $argParent->name->toLowerString(); - } - if ( - $functionName === 'array_map' - && isset($args[1]) - ) { + $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); - if (!isset($args[2])) { - $callbackParameters = [ - new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ]; - } else { - $callbackParameters = []; - foreach ($args as $i => $arg) { - if ($i === 0) { - continue; + $callbackParameters = []; + foreach ($arrayMapArgs as $arg) { + $argType = $scope->getType($arg->value); + if ($arg->unpack) { + $constantArrays = $argType->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + $valueTypes = $constantArray->getValueTypes(); + foreach ($valueTypes as $valueType) { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), false, PassedByReference::createNo(), false, null); + } + } } - - $callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } else { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null); } } $parameters[0] = new NativeParameterReflection( $parameters[0]->getName(), $parameters[0]->isOptional(), - new CallableType($callbackParameters, new MixedType(), false), + new UnionType([ + new CallableType($callbackParameters, new MixedType(), false), + new NullType(), + ]), $parameters[0]->passedByReference(), $parameters[0]->isVariadic(), - $parameters[0]->getDefaultValue() + $parameters[0]->getDefaultValue(), ); $parametersAcceptors = [ new FunctionVariant( @@ -91,26 +118,56 @@ public static function selectFromArgs( $acceptor->getResolvedTemplateTypeMap(), $parameters, $acceptor->isVariadic(), - $acceptor->getReturnType() + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } - if ( - $functionName === 'array_filter' - && isset($args[0]) - ) { + if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { + $optType = $scope->getType($args[1]->value); + if ($optType instanceof ConstantIntegerType) { + $optValueType = self::getCurlOptValueType($optType->getValue()); + + if ($optValueType !== null) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + + $parameters[2] = new NativeParameterReflection( + $parameters[2]->getName(), + $parameters[2]->isOptional(), + $optValueType, + $parameters[2]->passedByReference(), + $parameters[2]->isVariadic(), + $parameters[2]->getDefaultValue(), + ); + + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + + if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { if (isset($args[2])) { $mode = $scope->getType($args[2]->value); if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } } @@ -118,55 +175,325 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new UnionType([ + new CallableType( + $arrayFilterParameters ?? [ + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + new NullType(), + ]), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if (count($args) <= 2 && (bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $namedArgumentsVariants[0] ?? $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + if (isset($args[1]) || ($args[0]->name !== null && $args[0]->name->name === 'array')) { + $parameters = [ + new NativeParameterReflection($parameters[0]->getName(), false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection($parameters[1]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null), + ]; + } else { + $parameters = [ + new NativeParameterReflection($parameters[0]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null), + ]; + } + + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if ((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 ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $argType = $scope->getType($args[0]->value); $parameters[1] = new NativeParameterReflection( $parameters[1]->getName(), $parameters[1]->isOptional(), new CallableType( - $arrayFilterParameters ?? [ - new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), + [ + new DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), ], - new MixedType(), - false + new BooleanType(), + false, ), $parameters[1]->passedByReference(), $parameters[1]->isVariadic(), - $parameters[1]->getDefaultValue() + $parameters[1]->getDefaultValue(), ); $parametersAcceptors = [ new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), $acceptor->isVariadic(), - $acceptor->getReturnType() + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } + + $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + if ( + $closureBindToVar instanceof Node\Expr\Variable + && is_string($closureBindToVar->name) + ) { + $varType = $scope->getType($closureBindToVar); + if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $closureThisParameters = []; + foreach ($inFunction->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureBindToVar->name, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureBindToVar->name))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + $closureThisParameters[$closureBindToVar->name], + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + + if ( + $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null + && $args[0]->value instanceof Node\Expr\Variable + && is_string($args[0]->value->name) + ) { + $closureVarName = $args[0]->value->name; + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $closureThisParameters = []; + foreach ($inFunction->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureVarName, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureVarName))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + $closureThisParameters[$closureVarName], + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + + if (count($parametersAcceptors) === 1) { + $acceptor = $parametersAcceptors[0]; + if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { + return $acceptor; + } + } + + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; } - foreach ($args as $arg) { - $type = $scope->getType($arg->value); - if ($arg->unpack) { + $hasName = false; + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = $parameters[count($parameters) - 1]; + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); + $hasName = true; + } else { + $index = $i; + } + if ($originalArg->unpack) { $unpack = true; - $types[] = $type->getIterableValueType(); + $types[$index] = $type->getIterableValueType(); } else { - $types[] = $type; + $types[$index] = $type; } } + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + return self::selectFromTypes($types, $parametersAcceptors, $unpack); } + private static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool + { + if (self::hasTemplateOrLateResolvableType($acceptor->getReturnType())) { + return true; + } + + foreach ($acceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getOutType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getOutType()) + ) { + return true; + } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getClosureThisType()) + ) { + return true; + } + + if (!self::hasTemplateOrLateResolvableType($parameter->getType())) { + continue; + } + + return true; + } + + return false; + } + + private static function hasTemplateOrLateResolvableType(Type $type): bool + { + $has = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$has): Type { + if ($type instanceof TemplateType || $type instanceof LateResolvableType) { + $has = true; + return $type; + } + + return $traverse($type); + }); + + return $has; + } + /** - * @param \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) { @@ -174,8 +501,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.', ); } @@ -231,7 +558,7 @@ public static function selectFromTypes( break; } - $type = $types[count($types) - 1]; + $type = $types[array_key_last($types)]; } else { $type = $types[$i]; } @@ -239,7 +566,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); } } @@ -265,22 +592,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; @@ -303,25 +629,52 @@ 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(); + $mustUseReturnValue = TrinaryLogic::createMaybe(); 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()); + $mustUseReturnValue = $mustUseReturnValue->or($acceptor->mustUseReturnValue()); } $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; } @@ -335,13 +688,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) { @@ -351,13 +743,387 @@ 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, + $mustUseReturnValue, + ); + } + + 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(), + $acceptor->mustUseReturnValue(), + ); + } + + 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 de4c7e675f..0000000000 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public function getParameters(): array; - - public function getPhpDocReturnType(): Type; - - public function getNativeReturnType(): Type; - -} diff --git a/src/Reflection/PassedByReference.php b/src/Reflection/PassedByReference.php index 663dcbe3fb..804d049b43 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -2,8 +2,12 @@ namespace PHPStan\Reflection; -/** @api */ -class PassedByReference +use function array_key_exists; + +/** + * @api + */ +final class PassedByReference { private const NO = 1; @@ -13,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 @@ -75,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 266d002075..0000000000 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ /dev/null @@ -1,49 +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,49 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->nativeMethodReflection->mustUseReturnValue(); + } + } diff --git a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php index 3ec871b59e..abeb693630 100644 --- a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -2,22 +2,16 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\Type; -class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private UnresolvedMethodPrototypeReflection $prototype; - - private ClosureType $closure; - - public function __construct(UnresolvedMethodPrototypeReflection $prototype, ClosureType $closure) + public function __construct(private UnresolvedMethodPrototypeReflection $prototype, private ClosureType $closure) { - $this->prototype = $prototype; - $this->closure = $closure; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -25,12 +19,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return new self($this->prototype->doNotResolveTemplateTypeMapToBounds(), $this->closure); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { return new ClosureCallMethodReflection($this->prototype->getTransformedMethod(), $this->closure); } diff --git a/src/Reflection/Php/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..d36306a1b5 --- /dev/null +++ b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php @@ -0,0 +1,30 @@ +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..91d795598b --- /dev/null +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -0,0 +1,169 @@ +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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Reflection/Php/EnumPropertyReflection.php b/src/Reflection/Php/EnumPropertyReflection.php new file mode 100644 index 0000000000..41ee8e0b5c --- /dev/null +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -0,0 +1,160 @@ +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 isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} 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..c4ab5219df --- /dev/null +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -0,0 +1,151 @@ +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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} 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 cfb2d1b831..0000000000 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ /dev/null @@ -1,116 +0,0 @@ -methodName = $methodName; - $this->declaringClass = $declaringClass; - } - - public function getName(): string - { - return $this->methodName; - } - - public function getReflection(): ?\ReflectionMethod - { - return null; - } - - public function getFileName(): ?string - { - return null; - } - - public function getDeclaringClass(): \ReflectionClass - { - return $this->declaringClass; - } - - public function getStartLine(): ?int - { - return null; - } - - public function getEndLine(): ?int - { - return null; - } - - public function getDocComment(): ?string - { - return null; - } - - public function isStatic(): bool - { - return false; - } - - public function isPrivate(): bool - { - return false; - } - - public function isPublic(): bool - { - return true; - } - - public function getPrototype(): BuiltinMethodReflection - { - throw new \ReflectionException(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isVariadic(): bool - { - return false; - } - - public function isFinal(): bool - { - return false; - } - - public function isInternal(): bool - { - return false; - } - - public function isAbstract(): bool - { - return false; - } - - public function getReturnType(): ?\ReflectionType - { - return null; - } - - /** - * @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 692c06a010..0000000000 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ /dev/null @@ -1,135 +0,0 @@ -reflection = $reflection; - } - - public function getName(): string - { - return $this->reflection->getName(); - } - - public function getReflection(): ?\ReflectionMethod - { - return $this->reflection; - } - - public function getFileName(): ?string - { - $fileName = $this->reflection->getFileName(); - if ($fileName === false) { - return null; - } - - return $fileName; - } - - public function getDeclaringClass(): \ReflectionClass - { - return $this->reflection->getDeclaringClass(); - } - - public function getStartLine(): ?int - { - $line = $this->reflection->getStartLine(); - if ($line === false) { - return null; - } - - return $line; - } - - public function getEndLine(): ?int - { - $line = $this->reflection->getEndLine(); - if ($line === false) { - return null; - } - - return $line; - } - - public function getDocComment(): ?string - { - $docComment = $this->reflection->getDocComment(); - if ($docComment === false) { - return null; - } - - return $docComment; - } - - public function isStatic(): bool - { - return $this->reflection->isStatic(); - } - - public function isPrivate(): bool - { - return $this->reflection->isPrivate(); - } - - public function isPublic(): bool - { - return $this->reflection->isPublic(); - } - - public function isConstructor(): bool - { - return $this->reflection->isConstructor(); - } - - public function getPrototype(): BuiltinMethodReflection - { - return new self($this->reflection->getPrototype()); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); - } - - public function isFinal(): bool - { - return $this->reflection->isFinal(); - } - - public function isInternal(): bool - { - return $this->reflection->isInternal(); - } - - public function isAbstract(): bool - { - return $this->reflection->isAbstract(); - } - - public function isVariadic(): bool - { - return $this->reflection->isVariadic(); - } - - public function getReturnType(): ?\ReflectionType - { - return $this->reflection->getReturnType(); - } - - /** - * @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 0692f245c4..a8a6e5a948 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -8,9 +8,11 @@ use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Namespace_; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\OutOfClassScope; use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\ScopeFactory; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\Parser\Parser; use PHPStan\PhpDoc\PhpDocInheritanceResolver; @@ -18,73 +20,64 @@ use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +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; - -class PhpClassReflectionExtension - implements PropertiesClassReflectionExtension, MethodsClassReflectionExtension +use PHPStan\Type\UnionType; +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 { - private ScopeFactory $scopeFactory; - - private NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension; - - private \PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension; - - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider; - - private bool $inferPrivatePropertyTypeFromConstructor; - - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private FileTypeMapper $fileTypeMapper; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - /** @var \PHPStan\Reflection\PropertyReflection[][] */ + /** @var PhpPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection[][] */ + /** @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> */ @@ -93,50 +86,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 ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider - * @param FileTypeMapper $fileTypeMapper - * @param bool $inferPrivatePropertyTypeFromConstructor - * @param string[] $universalObjectCratesClasses - */ public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - PhpMethodReflectionFactory $methodReflectionFactory, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, - AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, - SignatureMapProvider $signatureMapProvider, - Parser $parser, - StubPhpDocProvider $stubPhpDocProvider, - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - FileTypeMapper $fileTypeMapper, - bool $inferPrivatePropertyTypeFromConstructor, - array $universalObjectCratesClasses + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + private PhpMethodReflectionFactory $methodReflectionFactory, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private 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->reflectionProviderProvider = $reflectionProviderProvider; - $this->fileTypeMapper = $fileTypeMapper; - $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 @@ -144,20 +158,23 @@ 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, ClassMemberAccessAnswerer $scope): PhpPropertyReflection { - if (!isset($this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { - $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, true); + $cacheKey = $classReflection->getCacheKey(); + if ($scope->isInClass()) { + $cacheKey = sprintf('%s-%s', $cacheKey, $scope->getClassReflection()->getCacheKey()); + } + if (!isset($this->propertiesIncludingAnnotations[$cacheKey][$propertyName])) { + $this->propertiesIncludingAnnotations[$cacheKey][$propertyName] = $this->createProperty($classReflection, $propertyName, $scope, true); } - return $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; + return $this->propertiesIncludingAnnotations[$cacheKey][$propertyName]; } public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection { if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) { - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection $property */ - $property = $this->createProperty($classReflection, $propertyName, false); + $property = $this->createProperty($classReflection, $propertyName, new OutOfClassScope(), false); $this->nativeProperties[$classReflection->getCacheKey()][$propertyName] = $property; } @@ -167,91 +184,90 @@ public function getNativeProperty(ClassReflection $classReflection, string $prop private function createProperty( ClassReflection $classReflection, string $propertyName, - bool $includingAnnotations - ): PropertyReflection + ClassMemberAccessAnswerer $scope, + bool $includingAnnotations, + ): PhpPropertyReflection { $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; - $isInternal = 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; + } - if ($includingAnnotations && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName)) { - $hierarchyDistances = $classReflection->getClassHierarchyDistances(); - $annotationProperty = $this->annotationsPropertiesClassReflectionExtension->getProperty($classReflection, $propertyName); - if (!isset($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()])) { - throw new \PHPStan\ShouldNotHappenException(); - } + $case = $classReflection->getEnumCase($name); + $value = $case->getBackingValueType(); + if ($value === null) { + throw new ShouldNotHappenException(); + } - $distanceDeclaringClass = $propertyReflection->getDeclaringClass()->getName(); - $propertyTrait = $this->findPropertyTrait($propertyReflection); - if ($propertyTrait !== null) { - $distanceDeclaringClass = $propertyTrait; - } - if (!isset($hierarchyDistances[$distanceDeclaringClass])) { - throw new \PHPStan\ShouldNotHappenException(); - } + $types[] = $value; + } + + $phpDocType = TypeCombinator::union(...$types); - if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { - return $annotationProperty; + return new PhpPropertyReflection($declaringClassReflection, null, new MixedType(), $phpDocType, $phpDocType, $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, [], false, true, false, false, true); } } + $deprecation = $this->deprecationProvider->getPropertyDeprecation($propertyReflection); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $isInternal = false; + $isReadOnlyByPhpDoc = $classReflection->isImmutable(); + $isFinal = $classReflection->isFinal() || $propertyReflection->isFinal(); + $isAllowedPrivateMutation = false; + $docComment = $propertyReflection->getDocComment() !== false ? $propertyReflection->getDocComment() : null; - $declaringTraitName = null; $phpDocType = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findPropertyPhpDoc( - $declaringClassName, - $propertyReflection->getName() - ); - $stubPhpDocString = null; - if ($resolvedPhpDoc === null) { - if ($declaringClassReflection->getFileName() !== null) { - $declaringTraitName = $this->findPropertyTrait($propertyReflection); - $constructorName = null; - if (method_exists($propertyReflection, 'isPromoted') && $propertyReflection->isPromoted()) { - if ($declaringClassReflection->hasConstructor()) { - $constructorName = $declaringClassReflection->getConstructor()->getName(); - } - } - - if ($constructorName === null) { - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( - $docComment, - $declaringClassReflection, - $declaringClassReflection->getFileName(), - $declaringTraitName, - $propertyName - ); - } elseif ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClassReflection->getFileName(), - $declaringClassName, - $declaringTraitName, - $constructorName, - $docComment - ); - } - $phpDocBlockClassReflection = $declaringClassReflection; + $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) { @@ -259,65 +275,64 @@ private function createProperty( } elseif (isset($varTags[$propertyName])) { $phpDocType = $varTags[$propertyName]->getType(); } + + $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( + $phpDocType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), + ) : null; + + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isReadOnlyByPhpDoc = $isReadOnlyByPhpDoc || $resolvedPhpDoc->isReadOnly(); + $isFinal = $isFinal || $resolvedPhpDoc->isFinal(); + $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); } if ($phpDocType === null) { - if (isset($constructorName) && $declaringClassReflection->getFileName() !== null) { + if (isset($constructorName)) { $constructorDocComment = $declaringClassReflection->getConstructor()->getDocComment(); $nativeClassReflection = $declaringClassReflection->getNativeReflection(); $positionalParameterNames = []; if ($nativeClassReflection->getConstructor() !== null) { - $positionalParameterNames = array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $nativeClassReflection->getConstructor()->getParameters()); + $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $nativeClassReflection->getConstructor()->getParameters()); } - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $resolvedConstructorPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $constructorDocComment, $declaringClassReflection->getFileName(), $declaringClassReflection, $declaringTraitName, $constructorName, - $positionalParameterNames + $positionalParameterNames, ); - $paramTags = $resolvedPhpDoc->getParamTags(); + $paramTags = $resolvedConstructorPhpDoc->getParamTags(); if (isset($paramTags[$propertyReflection->getName()])) { $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); } } } - if ($resolvedPhpDoc !== null) { - if (!isset($phpDocBlockClassReflection)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( - $phpDocType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() - ) : null; - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - } - if ( $phpDocType === null && $this->inferPrivatePropertyTypeFromConstructor && $declaringClassReflection->getFileName() !== null && $propertyReflection->isPrivate() - && (!method_exists($propertyReflection, 'hasType') || !$propertyReflection->hasType()) + && !$propertyReflection->isPromoted() + && !$propertyReflection->hasType() && $declaringClassReflection->hasConstructor() && $declaringClassReflection->getConstructor()->getDeclaringClass()->getName() === $declaringClassReflection->getName() ) { $phpDocType = $this->inferPrivatePropertyType( $propertyReflection->getName(), - $declaringClassReflection->getConstructor() + $declaringClassReflection->getConstructor(), ); } - $nativeType = null; - if (method_exists($propertyReflection, 'getType') && $propertyReflection->getType() !== null) { - $nativeType = $propertyReflection->getType(); - } + $nativeType = TypehintHelper::decideTypeFromReflection($propertyReflection->getType(), selfClass: $declaringClassReflection); $declaringTrait = null; $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); @@ -327,17 +342,144 @@ private function createProperty( $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } - return new PhpPropertyReflection( + $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); + } + } + } + } + + $nativeProperty = new PhpPropertyReflection( $declaringClassReflection, $declaringTrait, $nativeType, $phpDocType, + $phpDocType, $propertyReflection, + $getHook, + $setHook, $deprecatedDescription, $isDeprecated, $isInternal, - $stubPhpDocString + $isReadOnlyByPhpDoc, + $isAllowedPrivateMutation, + $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), + $isFinal, + true, + true, + $propertyReflection->isPrivate(), + $propertyReflection->isPublic(), ); + + if ( + $includingAnnotations + && !$declaringClassReflection->isEnum() + && !$propertyReflection->isStatic() + && ($classReflection->allowsDynamicProperties() || $scope->canReadProperty($nativeProperty)) + && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName) + && ( + $nativeProperty->isPublic() + || ($scope->isInClass() && $scope->getClassReflection()->getName() !== $declaringClassReflection->getName()) + ) + ) { + $hierarchyDistances = $classReflection->getClassHierarchyDistances(); + $annotationProperty = $this->annotationsPropertiesClassReflectionExtension->getProperty($classReflection, $propertyName); + if (!isset($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()])) { + throw new ShouldNotHappenException(); + } + + $distanceDeclaringClass = $propertyReflection->getDeclaringClass()->getName(); + $propertyTrait = $this->findPropertyTrait($propertyReflection); + if ($propertyTrait !== null) { + $distanceDeclaringClass = $propertyTrait; + } + if (!isset($hierarchyDistances[$distanceDeclaringClass])) { + throw new ShouldNotHappenException(); + } + + if ( + $hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass] + ) { + if ($nativeType->isSuperTypeOf($annotationProperty->getReadableType())->yes()) { + $nativeType = new MixedType(); + } + + return new PhpPropertyReflection( + $annotationProperty->getDeclaringClass(), + $declaringTrait, + $nativeType, + $annotationProperty->getReadableType(), + $annotationProperty->getWritableType(), + $propertyReflection, + $getHook, + $setHook, + $deprecatedDescription, + $isDeprecated, + $isInternal, + $isReadOnlyByPhpDoc, + $isAllowedPrivateMutation, + $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), + $isFinal, + $annotationProperty->isReadable(), + $annotationProperty->isWritable(), + false, + true, + ); + } + } + + return $nativeProperty; } public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -345,13 +487,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; @@ -365,49 +507,21 @@ public function getMethod(ClassReflection $classReflection, string $methodName): public function hasNativeMethod(ClassReflection $classReflection, string $methodName): bool { - $hasMethod = $this->hasMethod($classReflection, $methodName); - if ($hasMethod) { - return true; - } - - if ($methodName === '__get' && UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection - )) { - return true; - } - - return false; + return $this->hasMethod($classReflection, $methodName); } - public function getNativeMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getNativeMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { if (isset($this->nativeMethods[$classReflection->getCacheKey()][$methodName])) { return $this->nativeMethods[$classReflection->getCacheKey()][$methodName]; } - if ($classReflection->getNativeReflection()->hasMethod($methodName)) { - $nativeMethodReflection = new NativeBuiltinMethodReflection( - $classReflection->getNativeReflection()->getMethod($methodName) - ); - } else { - if ( - $methodName !== '__get' - || !UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection - )) { - throw new \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; @@ -418,15 +532,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(); @@ -435,10 +549,10 @@ 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; } } @@ -446,108 +560,162 @@ private function createMethod( $declaringClass = $classReflection->getAncestorWithClassName($declaringClassName); if ($declaringClass === null) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Internal error: Expected to find an ancestor with class name %s on %s, but none was found.', $declaringClassName, - $classReflection->getName() + $classReflection->getName(), )); } - if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { - $variantNumbers = [0]; - $i = 1; - while ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName(), $i)) { - $variantNumbers[] = $i; - $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; + return new EnumCasesMethodReflection($declaringClass, $arrayBuilder->getArray()); + } + + if (($declaringClass->isBuiltin() || $declaringClass->isEnum()) && $this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { + $variantsByType = ['positional' => []]; $throwType = null; - if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { - $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); - } elseif (class_exists($classReflection->getName(), false)) { - $reflectionClass = new \ReflectionClass($classReflection->getName()); - if ($reflectionClass->hasMethod($methodReflection->getName())) { - $reflectionMethod = $reflectionClass->getMethod($methodReflection->getName()); - } - } - foreach ($variantNumbers as $variantNumber) { - $methodSignature = $this->signatureMapProvider->getMethodSignature($declaringClassName, $methodReflection->getName(), $reflectionMethod, $variantNumber); - $phpDocParameterNameMapping = []; - foreach ($methodSignature->getParameters() as $parameter) { - $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; + $selfOutType = null; + $phpDocComment = null; + $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection); + foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { + if ($methodSignatures === null) { + continue; } - $stubPhpDocReturnType = null; - $stubPhpDocParameterTypes = []; - $stubPhpDocParameterVariadicity = []; - $phpDocParameterTypes = []; - $phpDocReturnType = null; - $stubPhpDocPair = null; - if (count($variantNumbers) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static 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 = TemplateTypeHelper::resolveTemplateTypes( - $returnTag->getType(), - $templateTypeMap - ); - } - foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { - $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramTag->getType(), - $templateTypeMap - ); - $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); - } + foreach ($methodSignatures as $methodSignature) { + $phpDocParameterNameMapping = []; + foreach ($methodSignature->getParameters() as $parameter) { + $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + } + $stubPhpDocReturnType = null; + $stubPhpDocParameterTypes = []; + $stubPhpDocParameterVariadicity = []; + $phpDocParameterTypes = []; + $phpDocReturnType = null; + $stubPhpDocPair = null; + $stubPhpParameterOutTypes = []; + $phpDocParameterOutTypes = []; + $immediatelyInvokedCallableParameters = []; + $closureThisParameters = []; + $stubImmediatelyInvokedCallableParameters = []; + $stubClosureThisParameters = []; + if (count($methodSignatures) === 1) { + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); + if ($stubPhpDocPair !== null) { + [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; + $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $stubDeclaringClass->getCallSiteVarianceMap(); + $returnTag = $stubPhpDoc->getReturnTag(); + $stubImmediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $stubPhpDoc->getParamsImmediatelyInvokedCallable()); + if ($returnTag !== null) { + $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( + $returnTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } - $throwsTag = $stubPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); + $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 && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { - $filename = $reflectionMethod->getFileName(); - if ($filename !== false) { - $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( - $filename, - $declaringClassName, - null, - $reflectionMethod->getName(), - $reflectionMethod->getDocComment() - ); - $throwsTag = $phpDocBlock->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - $returnTag = $phpDocBlock->getReturnTag(); - if ($returnTag !== null) { - $phpDocReturnType = $returnTag->getType(); - } - foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { - $phpDocParameterTypes[$name] = $paramTag->getType(); - } + if ($stubPhpDocPair === null && $methodReflection->getDocComment() !== false) { + $filename = $methodReflection->getFileName(); + if ($filename !== false) { + $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $filename, + $declaringClassName, + null, + $methodReflection->getName(), + $methodReflection->getDocComment(), + ); + $throwsTag = $phpDocBlock->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } + $returnTag = $phpDocBlock->getReturnTag(); + if ($returnTag !== null && count($methodSignatures) === 1) { + $phpDocReturnType = $returnTag->getType(); + } + $immediatelyInvokedCallableParameters = array_map(static fn ($immediate) => TrinaryLogic::createFromBoolean($immediate), $phpDocBlock->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $phpDocBlock->getParamClosureThisTags()); + foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { + $phpDocParameterTypes[$name] = $paramTag->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + $acceptsNamedArguments = $phpDocBlock->acceptsNamedArguments(); + + $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } - $signatureParameters = $methodSignature->getParameters(); - foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { - if (!array_key_exists($paramI, $signatureParameters)) { - continue; + if ($phpDocBlock->hasPhpDocString()) { + $phpDocComment = $phpDocBlock->getPhpDocString(); } - $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); + } + + $signatureParameters = $methodSignature->getParameters(); + foreach ($methodReflection->getParameters() as $paramI => $reflectionParameter) { + if (!array_key_exists($paramI, $signatureParameters)) { + continue; + } + + $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + } } } + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $stubImmediatelyInvokedCallableParameters, $immediatelyInvokedCallableParameters, $stubClosureThisParameters, $closureThisParameters, $signatureType !== 'named'); } - $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping); } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { @@ -559,43 +727,69 @@ private function createMethod( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, - $variants, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $hasSideEffects, - $stubPhpDocString, - $throwType + $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() !== null) { - $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; @@ -606,22 +800,10 @@ private function createMethod( $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } - $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; - $phpDocReturnType = null; - $phpDocThrowType = null; - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; - $isFinal = false; - $isPure = false; - if ( - $methodReflection instanceof NativeBuiltinMethodReflection - && $methodReflection->isConstructor() - && $declaringClass->getFileName() !== null - ) { + if ($methodReflection->isConstructor()) { foreach ($methodReflection->getParameters() as $parameter) { - if (!method_exists($parameter, 'isPromoted') || !$parameter->isPromoted()) { + if (!$parameter->isPromoted()) { continue; } @@ -630,7 +812,7 @@ private function createMethod( } $parameterProperty = $methodReflection->getDeclaringClass()->getProperty($parameter->getName()); - if (!method_exists($parameterProperty, 'isPromoted') || !$parameterProperty->isPromoted()) { + if (!$parameterProperty->isPromoted()) { continue; } if ($parameterProperty->getDocComment() === false) { @@ -638,11 +820,11 @@ private function createMethod( } $propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClass->getFileName(), - $declaringClassName, + $fileDeclaringClass->getFileName(), + $fileDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), - $parameterProperty->getDocComment() + $parameterProperty->getDocComment(), ); $varTags = $propertyDocblock->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -656,36 +838,71 @@ private function createMethod( $phpDocParameterTypes[$parameter->getName()] = $phpDocType; } } - if ($resolvedPhpDoc !== null) { - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { - if (array_key_exists($paramName, $phpDocParameterTypes)) { - continue; - } - $phpDocParameterTypes[$paramName] = $paramTag->getType(); - } - foreach ($phpDocParameterTypes as $paramName => $paramType) { - $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( - $paramType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() - ); + + $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); + + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { + if (array_key_exists($paramName, $phpDocParameterTypes)) { + continue; } - $nativeReturnType = TypehintHelper::decideTypeFromReflection( - $methodReflection->getReturnType(), - null, - $declaringClass->getName() + $phpDocParameterTypes[$paramName] = $paramTag->getType(); + } + foreach ($phpDocParameterTypes as $paramName => $paramType) { + $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), ); - $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); - $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + } + + $phpDocParameterOutTypes = []; + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ); + } + + $nativeReturnType = TypehintHelper::decideTypeFromReflection( + $methodReflection->getReturnType(), + selfClass: $actualDeclaringClass, + ); + $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); + $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + if (!$isDeprecated) { $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + $isPure = null; + if ($actualDeclaringClass->isBuiltin() || $actualDeclaringClass->isEnum()) { + foreach (array_keys($actualDeclaringClass->getAncestors()) as $className) { + if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { + $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects']; + $isPure = !$hasSideEffects; + + break; + } + } + } + + $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, @@ -696,20 +913,29 @@ private function createMethod( $isDeprecated, $isInternal, $isFinal, - $stubPhpDocString, - $isPure + $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 $stubPhpDocReturnType * @param array $phpDocParameterTypes - * @param Type|null $phpDocReturnType * @param array $phpDocParameterNameMapping - * @return FunctionVariantWithPhpDocs + * @param array $stubPhpDocParameterOutTypes + * @param array $phpDocParameterOutTypes + * @param array $stubImmediatelyInvokedCallableParameters + * @param array $immediatelyInvokedCallableParameters + * @param array $stubClosureThisParameters + * @param array $closureThisParameters */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -718,13 +944,21 @@ private function createNativeMethodVariant( ?Type $stubPhpDocReturnType, array $phpDocParameterTypes, ?Type $phpDocReturnType, - array $phpDocParameterNameMapping - ): FunctionVariantWithPhpDocs + array $phpDocParameterNameMapping, + array $stubPhpDocParameterOutTypes, + array $phpDocParameterOutTypes, + array $stubImmediatelyInvokedCallableParameters, + array $immediatelyInvokedCallableParameters, + array $stubClosureThisParameters, + array $closureThisParameters, + bool $usePhpDocParameterNames, + ): ExtendedFunctionVariant { $parameters = []; foreach ($methodSignature->getParameters() as $parameterSignature) { $type = null; $phpDocType = null; + $parameterOutType = null; $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); @@ -733,168 +967,99 @@ private function createNativeMethodVariant( $phpDocType = $stubPhpDocParameterTypes[$parameterSignature->getName()]; } elseif (isset($phpDocParameterTypes[$phpDocParameterName])) { $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; + $type = TypehintHelper::decideType($parameterSignature->getType(), $phpDocType); + } + + if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { + $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; + } + + if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { + $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; + } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; + } else { + $immediatelyInvoked = TrinaryLogic::createMaybe(); + } + + $closureThisType = null; + if (isset($stubClosureThisParameters[$parameterSignature->getName()])) { + $closureThisType = $stubClosureThisParameters[$parameterSignature->getName()]; + } elseif (isset($closureThisParameters[$phpDocParameterName])) { + $closureThisType = $closureThisParameters[$phpDocParameterName]; } - $parameters[] = new NativeParameterWithPhpDocsReflection( - $phpDocParameterName, + $parameters[] = new ExtendedNativeParameterReflection( + $usePhpDocParameterNames + ? $phpDocParameterName + : $parameterSignature->getName(), $parameterSignature->isOptional(), $type ?? $parameterSignature->getType(), $phpDocType ?? new MixedType(), $parameterSignature->getNativeType(), $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), - null + $parameterSignature->getDefaultValue(), + $parameterOutType ?? $parameterSignature->getOutType(), + $immediatelyInvoked, + $closureThisType, + [], ); } - $returnType = null; if ($stubPhpDocReturnType !== null) { $returnType = $stubPhpDocReturnType; $phpDocReturnType = $stubPhpDocReturnType; + } else { + $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } - return new FunctionVariantWithPhpDocs( + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, $parameters, $methodSignature->isVariadic(), - $returnType ?? $methodSignature->getReturnType(), + $returnType, $phpDocReturnType ?? new MixedType(), - $methodSignature->getNativeReturnType() + $methodSignature->getNativeReturnType(), ); } - 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(); - } - - 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 + private function findPropertyTrait(ReflectionProperty $propertyReflection): ?string { - foreach ($traits as $trait) { - $result = $this->deepScanTraitsForProperty($trait->getTraits(), $propertyReflection); - if ($result !== null) { - return $result; + $declaringClass = $propertyReflection->getBetterReflection()->getDeclaringClass(); + if ($declaringClass->isTrait()) { + if ($propertyReflection->getDeclaringClass()->isTrait() && $propertyReflection->getDeclaringClass()->getName() === $declaringClass->getName()) { + return null; } - if (!$trait->hasProperty($propertyReflection->getName())) { - continue; - } - - $traitProperty = $trait->getProperty($propertyReflection->getName()); - if ($traitProperty->getDocComment() === $propertyReflection->getDocComment()) { - return $trait->getName(); - } + return $declaringClass->getName(); } return null; } private function findMethodTrait( - BuiltinMethodReflection $methodReflection + ReflectionMethod $methodReflection, ): ?string { - if ($methodReflection->getReflection() instanceof ReflectionMethod) { - $declaringClass = $methodReflection->getReflection()->getBetterReflection()->getDeclaringClass(); - if ($declaringClass->isTrait()) { - if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { - return null; - } - - return $declaringClass->getName(); - } - - return null; - } - - $declaringClass = $methodReflection->getDeclaringClass(); - if ( - $methodReflection->getFileName() === $declaringClass->getFileName() - && $methodReflection->getStartLine() >= $declaringClass->getStartLine() - && $methodReflection->getEndLine() <= $declaringClass->getEndLine() - ) { - return null; - } - - $declaringClass = $methodReflection->getDeclaringClass(); - $traitAliases = $declaringClass->getTraitAliases(); - if (array_key_exists($methodReflection->getName(), $traitAliases)) { - return explode('::', $traitAliases[$methodReflection->getName()])[0]; - } - - foreach ($this->collectTraits($declaringClass) as $traitReflection) { - if (!$traitReflection->hasMethod($methodReflection->getName())) { - continue; + $declaringClass = $methodReflection->getBetterReflection()->getDeclaringClass(); + if ($declaringClass->isTrait()) { + if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { + return null; } - if ( - $methodReflection->getFileName() === $traitReflection->getFileName() - && $methodReflection->getStartLine() >= $traitReflection->getStartLine() - && $methodReflection->getEndLine() <= $traitReflection->getEndLine() - ) { - return $traitReflection->getName(); - } + return $declaringClass->getName(); } return null; } - /** - * @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 + MethodReflection $constructor, ): ?Type { $declaringClassName = $constructor->getDeclaringClass()->getName(); @@ -912,11 +1077,10 @@ private function inferPrivatePropertyType( } /** - * @param \PHPStan\Reflection\MethodReflection $constructor * @return array */ private function inferAndCachePropertyTypes( - MethodReflection $constructor + MethodReflection $constructor, ): array { $declaringClass = $constructor->getDeclaringClass(); @@ -935,7 +1099,7 @@ private function inferAndCachePropertyTypes( } $methodNode = $this->findConstructorNode($constructor->getName(), $classNode->stmts); - if ($methodNode === null || $methodNode->stmts === null) { + if ($methodNode === null || $methodNode->stmts === null || count($methodNode->stmts) === 0) { return $this->propertyTypesCache[$declaringClass->getName()] = []; } @@ -945,14 +1109,12 @@ private function inferAndCachePropertyTypes( $namespace = implode('\\', array_slice($classNameParts, 0, -1)); } - $classScope = $this->scopeFactory->create( - ScopeContext::create($fileName), - false, - [], - $constructor, - $namespace - )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + $classScope = $this->scopeFactory->create(ScopeContext::create($fileName)); + if ($namespace !== null) { + $classScope = $classScope->enterNamespace($namespace); + } + $classScope = $classScope->enterClass($declaringClass); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -963,7 +1125,14 @@ private function inferAndCachePropertyTypes( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $propertyTypes = []; @@ -995,8 +1164,8 @@ private function inferAndCachePropertyTypes( continue; } - $propertyType = TypeUtils::generalizeType($propertyType, GeneralizePrecision::lessSpecific()); - if ($propertyType instanceof ConstantArrayType) { + $propertyType = $propertyType->generalize(GeneralizePrecision::lessSpecific()); + if ($propertyType->isConstantArray()->yes()) { $propertyType = new ArrayType(new MixedType(true), new MixedType(true)); } @@ -1007,15 +1176,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; @@ -1043,9 +1211,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 { @@ -1071,26 +1237,52 @@ 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()) { + if ($returnTag->isExplicit()) { + return $phpDocReturnType; + } + + if ($nativeReturnType->isSuperTypeOf($phpDocReturnType)->yes()) { return $phpDocReturnType; } + if ($phpDocReturnType instanceof UnionType) { + $types = []; + foreach ($phpDocReturnType->getTypes() as $innerType) { + if (!$nativeReturnType->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $types[] = $innerType; + } + + if (count($types) === 0) { + return null; + } + + return TypeCombinator::union(...$types); + } + return null; } /** - * @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]; } @@ -1107,7 +1299,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 d1c109af91..ba4e66fa1b 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -2,97 +2,86 @@ 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\TypehintHelper; -use PHPStan\Type\VoidType; - -class PhpFunctionFromParserNodeReflection implements \PHPStan\Reflection\FunctionReflection +use function array_reverse; +use function is_array; +use function is_string; +use function strtolower; + +/** + * @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 \PHPStan\Type\Type $realReturnType; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $throwType; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; - - private ?bool $isPure; + /** @var Function_|ClassMethod|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 Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure + * @param Function_|ClassMethod|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, - Type $realReturnType, - ?Type $phpDocReturnType = null, - ?Type $throwType = null, - ?string $deprecatedDescription = null, - bool $isDeprecated = false, - bool $isInternal = false, - bool $isFinal = false, - ?bool $isPure = null + private string $fileName, + private TemplateTypeMap $templateTypeMap, + private array $realParameterTypes, + private array $phpDocParameterTypes, + private array $realParameterDefaultValues, + private 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->realReturnType = $realReturnType; - $this->phpDocReturnType = $phpDocReturnType; - $this->throwType = $throwType; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->isPure = $isPure; + } + + /** + * @phpstan-assert-if-true PhpMethodFromParserNodeReflection $this + */ + public function isMethodOrPropertyHook(): bool + { + return $this instanceof PhpMethodFromParserNodeReflection; } protected function getFunctionLike(): FunctionLike @@ -100,54 +89,94 @@ 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, - $this->getParameters(), - $this->isVariadic(), - $this->getReturnType(), - $this->phpDocReturnType ?? new MixedType(), - $this->realReturnType - ), - ]; - } + return $this->variants ??= [ + new ExtendedFunctionVariant( + $this->getTemplateTypeMap(), + $this->getResolvedTemplateTypeMap(), + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType(), + $this->getPhpDocReturnType(), + $this->getNativeReturnType(), + ), + ]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->templateTypeMap; + } - return $this->variants; + 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, @@ -157,14 +186,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) { @@ -175,11 +208,26 @@ private function isVariadic(): bool return false; } - private function getReturnType(): Type + public function getReturnType(): Type { return TypehintHelper::decideType($this->realReturnType, $this->phpDocReturnType); } + public function getPhpDocReturnType(): Type + { + return $this->phpDocReturnType ?? new MixedType(); + } + + public function getNativeReturnType(): Type + { + return $this->realReturnType; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getDeprecatedDescription(): ?string { if ($this->isDeprecated) { @@ -199,15 +247,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; @@ -215,7 +254,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -230,4 +269,84 @@ 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; + } + + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 3193522627..e95de465ca 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -2,107 +2,75 @@ 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\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\GenerateFactory; +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\FunctionReflectionFactory; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; - -class PhpFunctionReflection implements FunctionReflection, ReflectionWithFilename +use function array_key_exists; +use function array_map; +use function count; +use function is_array; +use function is_file; +use function strtolower; + +#[GenerateFactory(interface: FunctionReflectionFactory::class)] +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; - - private ?string $filename; - - private ?bool $isPure; - - /** @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|null $filename - * @param bool|null $isPure + * @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, - ?string $filename, - ?bool $isPure = null + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionFunction $reflection, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + 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; - $this->isPure = $isPure; } public function getName(): string @@ -116,45 +84,58 @@ public function getFileName(): ?string return null; } - if (!file_exists($this->filename)) { + if (!is_file($this->filename)) { return null; } return $this->filename; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { - if ($this->variants === null) { - $this->variants = [ - new FunctionVariantWithPhpDocs( - $this->templateTypeMap, - null, - $this->getParameters(), - $this->isVariadic(), - $this->getReturnType(), - $this->getPhpDocReturnType(), - $this->getNativeReturnType() - ), - ]; - } + return $this->variants ??= [ + new ExtendedFunctionVariant( + $this->templateTypeMap, + null, + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType(), + $this->getPhpDocReturnType(), + $this->getNativeReturnType(), + ), + ]; + } - 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()); } @@ -162,75 +143,40 @@ private function getParameters(): array private function isVariadic(): bool { $isNativelyVariadic = $this->reflection->isVariadic(); - if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) { - $fileName = $this->reflection->getFileName(); - if (file_exists($fileName)) { - $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; - } + if (!$isNativelyVariadic && $this->reflection->getFileName() !== false && !$this->isBuiltin()) { + $filename = $this->reflection->getFileName(); - return $cachedResult; + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - } - 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; - } - - continue; + if (array_key_exists($this->reflection->getName(), VariadicFunctionsVisitor::$cache)) { + return $this->containsVariadicCalls = VariadicFunctionsVisitor::$cache[$this->reflection->getName()]; } - if ($node instanceof ClassLike) { - continue; - } + $nodes = $this->parser->parseFile($filename); + if (count($nodes) > 0) { + $variadicFunctions = $nodes[0]->getAttribute(VariadicFunctionsVisitor::ATTRIBUTE_NAME); - if ($node instanceof Namespace_) { - if ($this->callsFuncGetArgs($node->stmts)) { - return true; + if ( + is_array($variadicFunctions) + && array_key_exists($this->reflection->getName(), $variadicFunctions) + ) { + return $this->containsVariadicCalls = $variadicFunctions[$this->reflection->getName()]; } } - if (!$node instanceof Declare_ || $node->stmts === null) { - continue; - } - - if ($this->callsFuncGetArgs($node->stmts)) { - return true; - } + return $this->containsVariadicCalls = false; } - return false; + return $isNativelyVariadic; } private function getReturnType(): Type { return TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), - $this->phpDocReturnType + $this->phpDocReturnType, ); } @@ -254,13 +200,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(), ); } @@ -269,11 +220,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; @@ -281,7 +227,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -291,9 +237,53 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index ee5f5d6dd8..cd0e6fcbf6 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -2,66 +2,87 @@ 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 Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure + * @param Type[] $realParameterTypes + * @param Type[] $phpDocParameterTypes + * @param Type[] $realParameterDefaultValues + * @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, + array $parameterAttributes, Type $realReturnType, ?Type $phpDocReturnType, ?Type $throwType, ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, - ?bool $isPure = null + private bool $isFinal, + ?bool $isPure, + bool $acceptsNamedArguments, + Assertions $assertions, + private ?Type $selfOutType, + ?string $phpDocComment, + array $parameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, + private bool $isConstructor, + array $attributes, ) { + if ($this->classMethod instanceof Node\PropertyHook) { + if ($this->hookForProperty === null) { + throw new ShouldNotHappenException('Hook was provided but property was not'); + } + } elseif ($this->hookForProperty !== null) { + throw new ShouldNotHappenException('Hooked property was provided but hook was not'); + } + $name = strtolower($classMethod->name->name); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { + if ($this->isConstructor) { + $realReturnType = new VoidType(); + } + if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { @@ -76,23 +97,46 @@ public function __construct( if ($name === '__set_state') { $realReturnType = TypeCombinator::intersect(new ObjectWithoutClassType(), $realReturnType); } + if ($name === '__set') { + $realReturnType = new VoidType(); + } + + if ($name === '__debuginfo') { + $realReturnType = TypeCombinator::intersect(TypeCombinator::addNull( + new ArrayType(new MixedType(true), new MixedType(true)), + ), $realReturnType); + } + + if ($name === '__unserialize') { + $realReturnType = new VoidType(); + } + if ($name === '__serialize') { + $realReturnType = new ArrayType(new MixedType(true), new MixedType(true)); + } parent::__construct( $classMethod, + $fileName, $templateTypeMap, $realParameterTypes, $phpDocParameterTypes, $realParameterDefaultValues, + $parameterAttributes, $realReturnType, $phpDocReturnType, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal || $classMethod->isFinal(), - $isPure + $isPure, + $acceptsNamedArguments, + $assertions, + $phpDocComment, + $parameterOutTypes, + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $attributes, ); - $this->declaringClass = $declaringClass; } public function getDeclaringClass(): ClassReflection @@ -104,36 +148,107 @@ 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 isFinal(): TrinaryLogic + { + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->isFinal()); + } + + return TrinaryLogic::createFromBoolean($method->isFinal() || $this->isFinal); } - public function getDocComment(): ?string + public function isFinalByKeyword(): TrinaryLogic { - return null; + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isFinal()); } public function isBuiltin(): bool @@ -141,4 +256,44 @@ public function isBuiltin(): bool return false; } + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->returnsByRef()); + } + + public function isAbstract(): TrinaryLogic + { + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->body === null); + } + + return TrinaryLogic::createFromBoolean($method->isAbstract()); + } + + public function isConstructor(): bool + { + return $this->isConstructor; + } + + public function hasSideEffects(): TrinaryLogic + { + if ( + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() + ) { + return TrinaryLogic::createYes(); + } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 50cbd8d81a..3c4aafdd60 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -2,20 +2,25 @@ 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\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\GenerateFactory; +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,113 +30,75 @@ 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; - -/** @api */ -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 + */ +#[GenerateFactory(interface: PhpMethodReflectionFactory::class)] +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 bool $isInternal; + private ?Type $returnType = null; - private bool $isFinal; + private ?Type $nativeReturnType = null; - private ?bool $isPure; - - 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, - ?bool $isPure = null + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ClassReflection $declaringClass, + private ?ClassReflection $declaringTrait, + private ReflectionMethod $reflection, + private ReflectionProvider $reflectionProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + 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; - $this->isPure = $isPure; } public function getDeclaringClass(): ClassReflection @@ -144,23 +111,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(), selfClass: $prototypeDeclaringClass); + } return new MethodPrototypeReflection( $prototypeMethod->getName(), @@ -169,10 +139,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPrivate(), $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; } } @@ -187,6 +158,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); @@ -219,43 +194,48 @@ private function getMethodNameWithCorrectCase(string $lowercaseMethodName, strin } /** - * @return ParametersAcceptorWithPhpDocs[] + * @return list */ public function getVariants(): array { - if ($this->variants === null) { - $this->variants = [ - new FunctionVariantWithPhpDocs( - $this->templateTypeMap, - null, - $this->getParameters(), - $this->isVariadic(), - $this->getReturnType(), - $this->getPhpDocReturnType(), - $this->getNativeReturnType() - ), - ]; - } + return $this->variants ??= [ + new ExtendedFunctionVariant( + $this->templateTypeMap, + null, + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType(), + $this->getPhpDocReturnType(), + $this->getNativeReturnType(), + ), + ]; + } - 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()); - } - - return $this->parameters; + return $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()); } private function isVariadic(): bool @@ -268,84 +248,40 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== null && file_exists($filename)) { - $modifiedTime = filemtime($filename); - if ($modifiedTime === false) { - $modifiedTime = time(); - } - $key = sprintf('variadic-method-%s-%s-%s', $declaringClass->getName(), $this->reflection->getName(), $filename); - $variableCacheKey = sprintf('%d-v2', $modifiedTime); - $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; + if (!$isNativelyVariadic && $filename !== null && !$this->declaringClass->isBuiltin()) { + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - 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 @@ -362,32 +298,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, ); } @@ -405,15 +338,10 @@ private function getPhpDocReturnType(): Type private function getNativeReturnType(): Type { - if ($this->nativeReturnType === null) { - $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), - null, - $this->declaringClass->getName() - ); - } - - return $this->nativeReturnType; + return $this->nativeReturnType ??= TypehintHelper::decideTypeFromReflection( + $this->reflection->getReturnType(), + selfClass: $this->declaringClass, + ); } public function getDeprecatedDescription(): ?string @@ -422,22 +350,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 @@ -452,12 +399,9 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - $name = strtolower($this->getName()); - $isVoid = $this->getReturnType() instanceof VoidType; - if ( - $name !== '__construct' - && $isVoid + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() ) { return TrinaryLogic::createYes(); } @@ -465,11 +409,125 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createFromBoolean(!$this->isPure); } - if ($isVoid) { + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { return TrinaryLogic::createYes(); } return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean( + $this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments, + ); + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + + public function changePropertyGetHookPhpDocType(Type $phpDocType): self + { + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $this->phpDocParameterTypes, + $phpDocType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function changePropertySetHookPhpDocType(string $parameterName, Type $phpDocType): self + { + $phpDocParameterTypes = $this->phpDocParameterTypes; + $phpDocParameterTypes[$parameterName] = $phpDocType; + + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $phpDocParameterTypes, + $this->phpDocReturnType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 4d06c19570..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,26 +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 - * @param bool|null $isPure - * - * @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, @@ -38,8 +32,15 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?string $stubPhpDocString, - ?bool $isPure = null + ?bool $isPure, + Assertions $asserts, + ?Type $selfOutType, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, + bool $acceptsNamedArguments, + array $attributes, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 09c17d2a46..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,10 +50,10 @@ public function getType(): Type if ($this->type === null) { $phpDocType = $this->phpDocType; if ($phpDocType !== null && $this->defaultValue !== null) { - if ($this->defaultValue instanceof NullType) { + if ($this->defaultValue->isNull()->yes()) { $inferred = $phpDocType->inferTemplateTypes($this->defaultValue); if ($inferred->isEmpty()) { - $phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType); + $phpDocType = TypeCombinator::addNull($phpDocType); } } } @@ -79,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; @@ -99,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..8469f7bef4 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,32 +97,50 @@ public function getPhpDocType(): Type return new MixedType(); } - public function getNativeType(): Type + public function hasNativeType(): bool { - if ($this->nativeType === null) { - $this->nativeType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getType(), - null, - $this->declaringClassName, - $this->isVariadic() - ); - } + return $this->reflection->getType() !== null; + } - return $this->nativeType; + public function getNativeType(): Type + { + return $this->nativeType ??= TypehintHelper::decideTypeFromReflection( + $this->reflection->getType(), + selfClass: $this->declaringClass, + isVariadic: $this->isVariadic(), + ); } 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 87ff0fbdb8..ce6f8a2e37 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -2,60 +2,59 @@ namespace PHPStan\Reflection\Php; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; +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\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use function sprintf; -/** @api */ -class PhpPropertyReflection implements PropertyReflection +/** + * @api + */ +final class PhpPropertyReflection implements ExtendedPropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; + private ?Type $readableType = 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 $writableType = 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 Type $nativeType, + private ?Type $readablePhpDocType, + private ?Type $writablePhpDocType, + 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, + private bool $isFinal, + private bool $readable, + private bool $writable, + private bool $private, + private bool $public, ) { - $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 @@ -70,10 +69,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; @@ -89,64 +84,101 @@ public function isStatic(): bool public function isPrivate(): bool { - return $this->reflection->isPrivate(); + return $this->private; } public function isPublic(): bool { - return $this->reflection->isPublic(); + return $this->public; } public function isReadOnly(): bool { - if (method_exists($this->reflection, 'isReadOnly')) { - return $this->reflection->isReadOnly(); - } + return $this->reflection->isReadOnly(); + } - return false; + public function isReadOnlyByPhpDoc(): bool + { + return $this->isReadOnlyByPhpDoc; } public function getReadableType(): Type { - if ($this->type === null) { - $this->type = TypehintHelper::decideTypeFromReflection( + return $this->readableType ??= TypehintHelper::decideType( + $this->nativeType, + $this->readablePhpDocType, + ); + } + + public function getWritableType(): Type + { + if ($this->hasHook('set')) { + $setHookVariant = $this->getHook('set')->getOnlyVariant(); + $parameters = $setHookVariant->getParameters(); + if (isset($parameters[0])) { + return $parameters[0]->getType(); + } + } + + if ($this->writableType !== null) { + return $this->writableType; + } + + if ($this->writablePhpDocType === null || $this->writablePhpDocType instanceof NeverType) { + return $this->writableType = TypehintHelper::decideType( $this->nativeType, - $this->phpDocType, - $this->declaringClass->getName() + $this->readablePhpDocType, ); } - return $this->type; - } + if ( + $this->readablePhpDocType !== null + && !$this->readablePhpDocType->equals($this->writablePhpDocType) + ) { + return $this->writableType = $this->writablePhpDocType; + } - public function getWritableType(): Type - { - return $this->getReadableType(); + return $this->writableType = TypehintHelper::decideType( + $this->nativeType, + $this->writablePhpDocType, + ); } public function canChangeTypeAfterAssignment(): bool { - return true; - } + if ($this->isStatic()) { + return $this->getReadableType()->equals($this->getWritableType()); + } - public function isPromoted(): bool - { - if (!method_exists($this->reflection, 'isPromoted')) { + if ($this->isVirtual()->yes()) { + return false; + } + + if ($this->hasHook('get')) { + return false; + } + + if ($this->hasHook('set')) { return false; } + return $this->getReadableType()->equals($this->getWritableType()); + } + + public function isPromoted(): bool + { return $this->reflection->isPromoted(); } public function hasPhpDocType(): bool { - return $this->phpDocType !== null; + return $this->readablePhpDocType !== null; } public function getPhpDocType(): Type { - if ($this->phpDocType !== null) { - return $this->phpDocType; + if ($this->readablePhpDocType !== null) { + return $this->readablePhpDocType; } return new MixedType(); @@ -154,30 +186,46 @@ public function getPhpDocType(): Type public function hasNativeType(): bool { - return $this->nativeType !== null; + return !$this->nativeType instanceof MixedType || $this->nativeType->isExplicitMixed(); } public function getNativeType(): Type { - if ($this->finalNativeType === null) { - $this->finalNativeType = TypehintHelper::decideTypeFromReflection( - $this->nativeType, - null, - $this->declaringClass->getName() - ); - } - - return $this->finalNativeType; + return $this->nativeType; } public function isReadable(): bool { - return true; + if (!$this->readable) { + return false; + } + + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('get'); } public function isWritable(): bool { - return true; + if (!$this->writable) { + return false; + } + + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('set'); } public function getDeprecatedDescription(): ?string @@ -199,9 +247,85 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isInternal); } - public function getNativeReflection(): \ReflectionProperty + 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 isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->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; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/SealedAllowedSubTypesClassReflectionExtension.php b/src/Reflection/Php/SealedAllowedSubTypesClassReflectionExtension.php new file mode 100644 index 0000000000..a40ac80997 --- /dev/null +++ b/src/Reflection/Php/SealedAllowedSubTypesClassReflectionExtension.php @@ -0,0 +1,36 @@ +getSealedTags()) > 0; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + $types = []; + + foreach ($classReflection->getSealedTags() as $sealedTag) { + $type = $sealedTag->getType(); + if ($type instanceof UnionType) { + $types = $type->getTypes(); + } else { + $types = [$type]; + } + } + + return $types; + } + +} diff --git a/src/Reflection/Php/SimpleXMLElementProperty.php b/src/Reflection/Php/SimpleXMLElementProperty.php index 098e62a820..f9e47899c5 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,54 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/Soap/SoapClientMethodReflection.php b/src/Reflection/Php/Soap/SoapClientMethodReflection.php index 32701b305e..1017888c59 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodReflection.php +++ b/src/Reflection/Php/Soap/SoapClientMethodReflection.php @@ -12,17 +12,11 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -class SoapClientMethodReflection implements MethodReflection +final class SoapClientMethodReflection implements MethodReflection { - private ClassReflection $declaringClass; - - private string $name; - - public function __construct(ClassReflection $declaringClass, string $name) + public function __construct(private ClassReflection $declaringClass, private string $name) { - $this->declaringClass = $declaringClass; - $this->name = $name; } public function getDeclaringClass(): ClassReflection @@ -68,7 +62,7 @@ public function getVariants(): array TemplateTypeMap::createEmpty(), [], true, - new MixedType(true) + new MixedType(true), ), ]; } @@ -93,7 +87,7 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function getThrowType(): ?Type + public function getThrowType(): Type { return new ObjectType('SoapFault'); } diff --git a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php index 73924b6081..431026f938 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php +++ b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php @@ -6,12 +6,12 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; -class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - return $classReflection->getName() === 'SoapClient' || $classReflection->isSubclassOf('SoapClient'); + return $classReflection->is('SoapClient'); } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 5dbad67143..5247ef6784 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,54 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php index f488b85b4d..0cd79eb232 100644 --- a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php +++ b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php @@ -2,49 +2,56 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\MixedType; -class UniversalObjectCratesClassReflectionExtension - implements \PHPStan\Reflection\PropertiesClassReflectionExtension +final class UniversalObjectCratesClassReflectionExtension + implements PropertiesClassReflectionExtension { - /** @var string[] */ - private array $classes; - - private ReflectionProvider $reflectionProvider; - /** - * @param string[] $classes + * @param list $classes */ - public function __construct(ReflectionProvider $reflectionProvider, array $classes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private array $classes, + private AnnotationsPropertiesClassReflectionExtension $annotationClassReflection, + ) { - $this->reflectionProvider = $reflectionProvider; - $this->classes = $classes; } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return self::isUniversalObjectCrate( + return self::isUniversalObjectCrateImplementation( $this->reflectionProvider, $this->classes, - $classReflection + $classReflection, + ); + } + + public static function isUniversalObjectCrate( + ReflectionProvider $reflectionProvider, + ClassReflection $classReflection, + ): bool + { + 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) { @@ -52,10 +59,7 @@ public static function isUniversalObjectCrate( continue; } - if ( - $classReflection->getName() === $className - || $classReflection->isSubclassOf($className) - ) { + if ($classReflection->is($className)) { return true; } } @@ -65,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..294d71427a --- /dev/null +++ b/src/Reflection/PhpVersionStaticAccessor.php @@ -0,0 +1,29 @@ + $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 12b0041386..3b7387f85a 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -2,35 +2,38 @@ 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; public function getClassName(string $className): string; - public function supportsAnonymousClasses(): bool; - public function getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope + Node\Stmt\Class_ $classNode, + Scope $scope, ): ClassReflection; - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + /** @return list */ + public function getUniversalObjectCratesClasses(): array; + + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection; + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection; - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection; + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection; - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; } diff --git a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php b/src/Reflection/ReflectionProvider/ChainReflectionProvider.php deleted file mode 100644 index c2502f5efb..0000000000 --- a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php +++ /dev/null @@ -1,172 +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 supportsAnonymousClasses(): bool - { - foreach ($this->providers as $provider) { - if (!$provider->supportsAnonymousClasses()) { - continue; - } - - return true; - } - - return false; - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - foreach ($this->providers as $provider) { - if (!$provider->supportsAnonymousClasses()) { - continue; - } - - 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 b3ace3ff3f..0000000000 --- a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php +++ /dev/null @@ -1,173 +0,0 @@ -reflectionProvider = $reflectionProvider; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->patterns = $patterns; - $this->singleReflectionInsteadOfFile = $singleReflectionInsteadOfFile; - } - - public function hasClass(string $className): bool - { - if ($this->isClassBlacklisted($className)) { - return false; - } - - $has = $this->reflectionProvider->hasClass($className); - if (!$has) { - return false; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($this->singleReflectionInsteadOfFile !== null) { - if ($classReflection->getFileName() === $this->singleReflectionInsteadOfFile) { - return false; - } - } - - foreach ($classReflection->getParentClassesNames() as $parentClassName) { - if ($this->isClassBlacklisted($parentClassName)) { - return false; - } - } - - foreach ($classReflection->getNativeReflection()->getInterfaceNames() as $interfaceName) { - if ($this->isClassBlacklisted($interfaceName)) { - return false; - } - } - - return true; - } - - private function isClassBlacklisted(string $className): bool - { - if ($this->phpStormStubsSourceStubber->hasClass($className)) { - // check that userland class isn't aliased to the same name as a class from stubs - if (!class_exists($className, false)) { - return true; - } - if (in_array(strtolower($className), ['reflectionuniontype', 'attribute'], true)) { - return true; - } - $reflection = new \ReflectionClass($className); - if ($reflection->getFileName() === false) { - return true; - } - } - - foreach ($this->patterns as $pattern) { - if (Strings::match($className, $pattern) !== null) { - return true; - } - } - - return false; - } - - public function getClass(string $className): ClassReflection - { - if (!$this->hasClass($className)) { - throw new \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 supportsAnonymousClasses(): bool - { - return false; - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - throw new \PHPStan\ShouldNotHappenException(); - } - - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - $has = $this->reflectionProvider->hasFunction($nameNode, $scope); - if (!$has) { - return false; - } - - if ($this->singleReflectionInsteadOfFile === null) { - return true; - } - - $functionReflection = $this->reflectionProvider->getFunction($nameNode, $scope); - if (!$functionReflection instanceof ReflectionWithFilename) { - return true; - } - - return $functionReflection->getFileName() !== $this->singleReflectionInsteadOfFile; - } - - 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 751cb5160d..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,44 +56,44 @@ public function getClassName(string $className): string return $this->classNames[$lowerClassName] = $this->provider->getClassName($className); } - public function supportsAnonymousClasses(): bool + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - return $this->provider->supportsAnonymousClasses(); + return $this->provider->getAnonymousClassReflection($classNode, $scope); } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getUniversalObjectCratesClasses(): array { - return $this->provider->getAnonymousClassReflection($classNode, $scope); + return $this->provider->getUniversalObjectCratesClasses(); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - return $this->provider->hasFunction($nameNode, $scope); + return $this->provider->hasFunction($nameNode, $namespaceAnswerer); } - public function getFunction(\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..9f2e6424fd 100644 --- a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php +++ b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php @@ -2,39 +2,24 @@ namespace PHPStan\Reflection\ReflectionProvider; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; -class ReflectionProviderFactory +#[AutowiredService(name: '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 + #[AutowiredParameter(ref: '@betterReflectionProvider')] + 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 deleted file mode 100644 index 9e897084cb..0000000000 --- a/src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -reflectionProvider = $reflectionProvider; - } - - public function getReflectionProvider(): ReflectionProvider - { - return $this->reflectionProvider; - } - -} diff --git a/src/Reflection/ReflectionProviderStaticAccessor.php b/src/Reflection/ReflectionProviderStaticAccessor.php index b38c0ca69b..33ef7d0cf3 100644 --- a/src/Reflection/ReflectionProviderStaticAccessor.php +++ b/src/Reflection/ReflectionProviderStaticAccessor.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection; -class ReflectionProviderStaticAccessor +final class ReflectionProviderStaticAccessor { private static ?ReflectionProvider $instance = null; @@ -19,7 +19,7 @@ public static function registerInstance(ReflectionProvider $reflectionProvider): public static function getInstance(): ReflectionProvider { if (self::$instance === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingStaticAccessorInstanceException(); } return self::$instance; } diff --git a/src/Reflection/ReflectionWithFilename.php b/src/Reflection/ReflectionWithFilename.php deleted file mode 100644 index c626dbb7cf..0000000000 --- a/src/Reflection/ReflectionWithFilename.php +++ /dev/null @@ -1,11 +0,0 @@ -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..a4971b772f --- /dev/null +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -0,0 +1,129 @@ +findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()), + ) !== null; + } + + /** @deprecated Use getInstanceProperty or getStaticProperty */ + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()), + ); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + public function hasInstanceProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()), + ) !== null; + } + + public function getInstanceProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()), + ); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + public function hasStaticProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()), + ) !== null; + } + + public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()), + ); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + /** + * @param callable(Type, string): TrinaryLogic $propertyHasser + * @param callable(Type, string): ExtendedPropertyReflection $propertyGetter + */ + private function findProperty( + ClassReflection $classReflection, + string $propertyName, + callable $propertyHasser, + callable $propertyGetter, + ): ?ExtendedPropertyReflection + { + if (!$classReflection->isInterface()) { + return null; + } + + $requireExtendsTags = $classReflection->getRequireExtendsTags(); + foreach ($requireExtendsTags as $requireExtendsTag) { + $type = $requireExtendsTag->getType(); + + if (!$propertyHasser($type, $propertyName)->yes()) { + continue; + } + + return $propertyGetter($type, $propertyName); + } + + $interfaces = $classReflection->getInterfaces(); + foreach ($interfaces as $interface) { + $property = $this->findProperty($interface, $propertyName, $propertyHasser, $propertyGetter); + if ($property !== null) { + return $property; + } + } + + return null; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index cb75295c1b..92675f4f19 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,88 +2,13 @@ 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; - - /** @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 getOriginalParametersAcceptor(): ParametersAcceptor - { - return $this->parametersAcceptor; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(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 getReturnTypeWithUnresolvableTemplateTypes(): Type; } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php new file mode 100644 index 0000000000..17574ec30e --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -0,0 +1,121 @@ +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 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; + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; + } + +} 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..880fb448ac 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,68 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->reflection->mustUseReturnValue(); + } + } diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index f75c267964..2e0466446c 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -2,46 +2,41 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -class ResolvedPropertyReflection implements WrapperPropertyReflection +final class ResolvedPropertyReflection implements WrapperPropertyReflection { - private 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, + ) { - $this->reflection = $reflection; - $this->templateTypeMap = $templateTypeMap; } - public function getOriginalReflection(): PropertyReflection + public function getName(): string { - return $this->reflection; + return $this->reflection->getName(); } - public function getDeclaringClass(): ClassReflection + public function getOriginalReflection(): ExtendedPropertyReflection { - return $this->reflection->getDeclaringClass(); + return $this->reflection; } - public function getDeclaringTrait(): ?ClassReflection + public function getDeclaringClass(): ClassReflection { - if ($this->reflection instanceof PhpPropertyReflection) { - return $this->reflection->getDeclaringTrait(); - } - - return null; + return $this->reflection->getDeclaringClass(); } public function isStatic(): bool @@ -59,6 +54,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,11 +83,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->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $this->readableType = $type; @@ -89,11 +108,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->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $this->writableType = $type; @@ -136,4 +159,58 @@ public function isInternal(): TrinaryLogic return $this->reflection->isInternal(); } + public function isAbstract(): TrinaryLogic + { + return $this->reflection->isAbstract(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + + 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(); + } + + public function isDummy(): TrinaryLogic + { + return $this->reflection->isDummy(); + } + } diff --git a/src/Reflection/Runtime/RuntimeReflectionProvider.php b/src/Reflection/Runtime/RuntimeReflectionProvider.php deleted file mode 100644 index daf6fa24f9..0000000000 --- a/src/Reflection/Runtime/RuntimeReflectionProvider.php +++ /dev/null @@ -1,415 +0,0 @@ - */ - private array $cachedConstants = []; - - public function __construct( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - FunctionReflectionFactory $functionReflectionFactory, - FileTypeMapper $fileTypeMapper, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - PhpVersion $phpVersion, - NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, - StubPhpDocProvider $stubPhpDocProvider, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber - ) - { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->phpVersion = $phpVersion; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - } - - 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 supportsAnonymousClasses(): bool - { - return false; - } - - public function getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope - ): ClassReflection - { - throw new \PHPStan\ShouldNotHappenException(); - } - - /** - * @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->stubPhpDocProvider, - $this->phpDocInheritanceResolver, - $this->phpVersion, - $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), - $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), - $displayName, - $reflectionClass, - $anonymousFilename, - null, - $this->stubPhpDocProvider->findClassPhpDoc($className) - ); - $this->classReflections[$className] = $classReflection; - } - - return $this->classReflections[$className]; - } - - public function hasClass(string $className): bool - { - if (!ClassNameHelper::isValidClassName($className)) { - return $this->hasClassCache[$className] = false; - } - - $className = trim($className, '\\'); - if (isset($this->hasClassCache[$className])) { - return $this->hasClassCache[$className]; - } - - spl_autoload_register($autoloader = function (string $autoloadedClassName) use ($className): void { - $autoloadedClassName = trim($autoloadedClassName, '\\'); - if ($autoloadedClassName !== $className && !$this->isExistsCheckCall()) { - throw new \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; - $isPure = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); - if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { - $fileName = $reflectionFunction->getFileName(); - $docComment = $reflectionFunction->getDocComment(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); - } - - if ($resolvedPhpDoc !== null) { - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTags = $resolvedPhpDoc->getParamTags(); - $phpDocReturnTag = $resolvedPhpDoc->getReturnTag(); - $phpDocThrowsTag = $resolvedPhpDoc->getThrowsTag(); - $deprecatedTag = $resolvedPhpDoc->getDeprecatedTag(); - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); - } - - $functionReflection = $this->functionReflectionFactory->create( - $reflectionFunction, - $templateTypeMap, - array_map(static 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() !== false ? $reflectionFunction->getFileName() : null, - $isPure - ); - $this->customFunctionReflections[$lowerCasedFunctionName] = $functionReflection; - - return $functionReflection; - } - - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->resolveName($nameNode, function (string $name): bool { - $exists = function_exists($name) || $this->nativeFunctionReflectionProvider->findFunctionReflection($name) !== null; - if ($exists) { - if ($this->phpStormStubsSourceStubber->isPresentFunction($name) === false) { - return false; - } - - return true; - } - - return false; - }, $scope); - } - - public function hasConstant(\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); - } - - if (array_key_exists($constantName, $this->cachedConstants)) { - return $this->cachedConstants[$constantName]; - } - - return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( - $constantName, - ConstantTypeHelper::getTypeFromValue(constant($constantName)), - null - ); - } - - public function resolveConstantName(\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 2068335c5f..f9107d4b23 100644 --- a/src/Reflection/SignatureMap/FunctionSignature.php +++ b/src/Reflection/SignatureMap/FunctionSignature.php @@ -4,39 +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 \PHPStan\Type\Type $nativeReturnType; - - private bool $variadic; - /** - * @param array $parameters - * @param \PHPStan\Type\Type $returnType - * @param \PHPStan\Type\Type $nativeReturnType - * @param bool $variadic + * @param list $parameters */ public function __construct( - array $parameters, - Type $returnType, - Type $nativeReturnType, - bool $variadic + private array $parameters, + private Type $returnType, + private Type $nativeReturnType, + private bool $variadic, ) { - $this->parameters = $parameters; - $this->returnType = $returnType; - $this->nativeReturnType = $nativeReturnType; - $this->variadic = $variadic; } /** - * @return array + * @return list */ public function getParameters(): array { diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 0cdb5b1a21..3269191b79 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -2,53 +2,93 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\TypehintHelper; - -class FunctionSignatureMapProvider implements SignatureMapProvider +use ReflectionFunctionAbstract; +use function array_change_key_case; +use function array_key_exists; +use function array_keys; +use function is_array; +use function sprintf; +use function strtolower; +use const CASE_LOWER; + +#[AutowiredService(as: FunctionSignatureMapProvider::class)] +final class FunctionSignatureMapProvider implements SignatureMapProvider { - private \PHPStan\Reflection\SignatureMap\SignatureMapParser $parser; - - private PhpVersion $phpVersion; - - /** @var mixed[]|null */ - private ?array $signatureMap = null; + /** @var array */ + private static array $signatureMaps = []; /** @var array|null */ - private ?array $functionMetadata = null; + private static ?array $functionMetadata = null; + + public function __construct( + private SignatureMapParser $parser, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private PhpVersion $phpVersion, + #[AutowiredParameter(ref: '%featureToggles.stricterFunctionMap%')] + private bool $stricterFunctionMap, + ) + { + } + + public function hasMethodSignature(string $className, string $methodName): bool + { + return $this->hasFunctionSignature(sprintf('%s::%s', $className, $methodName)); + } - public function __construct(SignatureMapParser $parser, PhpVersion $phpVersion) + public function hasFunctionSignature(string $name): bool { - $this->parser = $parser; - $this->phpVersion = $phpVersion; + return array_key_exists(strtolower($name), $this->getSignatureMap()); } - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array { - return $this->hasFunctionSignature(sprintf('%s::%s', $className, $methodName), $variant); + return $this->getFunctionSignatures(sprintf('%s::%s', $className, $methodName), $className, $reflectionMethod); } - public function hasFunctionSignature(string $name, int $variant = 0): bool + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array { - $signatureMap = $this->getSignatureMap(); - if ($variant > 0) { - $name .= '\'' . $variant; + $functionName = strtolower($functionName); + + $signatures = [$this->createSignature($functionName, $className, $reflectionFunction)]; + $i = 1; + $variantFunctionName = $functionName . '\'' . $i; + while ($this->hasFunctionSignature($variantFunctionName)) { + $signatures[] = $this->createSignature($variantFunctionName, $className, $reflectionFunction); + $i++; + $variantFunctionName = $functionName . '\'' . $i; } - return array_key_exists(strtolower($name), $signatureMap); + + return ['positional' => $signatures, 'named' => null]; } - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + private function createSignature(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): FunctionSignature { - $signature = $this->getFunctionSignature(sprintf('%s::%s', $className, $methodName), $className, $variant); + if (!$reflectionFunction instanceof ReflectionMethod && !$reflectionFunction instanceof ReflectionFunction && $reflectionFunction !== null) { + throw new ShouldNotHappenException(); + } + $signatureMap = self::getSignatureMap(); + $signature = $this->parser->getFunctionSignature( + $signatureMap[$functionName], + $className, + ); $parameters = []; foreach ($signature->getParameters() as $i => $parameter) { - if ($reflectionMethod === null) { + if ($reflectionFunction === null) { $parameters[] = $parameter; continue; } - $nativeParameters = $reflectionMethod->getParameters(); + $nativeParameters = $reflectionFunction->getParameters(); if (!array_key_exists($i, $nativeParameters)) { $parameters[] = $parameter; continue; @@ -60,40 +100,26 @@ public function getMethodSignature(string $className, string $methodName, ?\Refl $parameter->getType(), TypehintHelper::decideTypeFromReflection($nativeParameters[$i]->getType()), $parameter->passedByReference(), - $parameter->isVariadic() + $parameter->isVariadic(), + $nativeParameters[$i]->isDefaultValueAvailable() ? $this->initializerExprTypeResolver->getType( + $nativeParameters[$i]->getDefaultValueExpression(), + InitializerExprContext::fromReflectionParameter($nativeParameters[$i]), + ) : null, + $parameter->getOutType(), ); } - if ($reflectionMethod === null) { + if ($reflectionFunction === null) { $nativeReturnType = new MixedType(); } else { - $nativeReturnType = TypehintHelper::decideTypeFromReflection($reflectionMethod->getReturnType()); + $nativeReturnType = TypehintHelper::decideTypeFromReflection($reflectionFunction->getReturnType()); } return new FunctionSignature( $parameters, $signature->getReturnType(), $nativeReturnType, - $signature->isVariadic() - ); - } - - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature - { - $functionName = strtolower($functionName); - if ($variant > 0) { - $functionName .= '\'' . $variant; - } - - if (!$this->hasFunctionSignature($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $signatureMap = self::getSignatureMap(); - - return $this->parser->getFunctionSignature( - $signatureMap[$functionName], - $className + $signature->isVariadic(), ); } @@ -104,13 +130,11 @@ public function hasMethodMetadata(string $className, string $methodName): bool public function hasFunctionMetadata(string $name): bool { - $signatureMap = $this->getFunctionMetadataMap(); + $signatureMap = self::getFunctionMetadataMap(); return array_key_exists(strtolower($name), $signatureMap); } /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array @@ -119,7 +143,6 @@ public function getMethodMetadata(string $className, string $methodName): array } /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array @@ -127,24 +150,24 @@ public function getFunctionMetadata(string $functionName): array $functionName = strtolower($functionName); if (!$this->hasFunctionMetadata($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->getFunctionMetadataMap()[$functionName]; + return self::getFunctionMetadataMap()[$functionName]; } /** * @return array */ - private function getFunctionMetadataMap(): array + private static function getFunctionMetadataMap(): array { - if ($this->functionMetadata === null) { + if (self::$functionMetadata === null) { /** @var array $metadata */ $metadata = require __DIR__ . '/../../../resources/functionMetadata.php'; - $this->functionMetadata = array_change_key_case($metadata, CASE_LOWER); + self::$functionMetadata = array_change_key_case($metadata, CASE_LOWER); } - return $this->functionMetadata; + return self::$functionMetadata; } /** @@ -152,36 +175,65 @@ private function getFunctionMetadataMap(): array */ public function getSignatureMap(): array { - if ($this->signatureMap === null) { - $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; - if (!is_array($signatureMap)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); - } + $cacheKey = sprintf('%d-%d', $this->phpVersion->getVersionId(), $this->stricterFunctionMap ? 1 : 0); + if (array_key_exists($cacheKey, self::$signatureMaps)) { + return self::$signatureMaps[$cacheKey]; + } - $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; + if (!is_array($signatureMap)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } - if ($this->phpVersion->getVersionId() >= 70400) { - $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; - if (!is_array($php74MapDelta)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); - $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); - } + if ($this->stricterFunctionMap) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_bleedingEdge.php'); + } - if ($this->phpVersion->getVersionId() >= 80000) { - $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; - if (!is_array($php80MapDelta)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); - } + 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'); - $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); + 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; + } - $this->signatureMap = $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->signatureMap; + return $this->computeSignatureMap($signatureMap, $signatureMapDelta); } /** @@ -201,4 +253,14 @@ private function computeSignatureMap(array $signatureMap, array $delta): array return $signatureMap; } + public function hasClassConstantMetadata(string $className, string $constantName): bool + { + return false; + } + + public function getClassConstantMetadata(string $className, string $constantName): array + { + throw new ShouldNotHappenException(); + } + } diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 2e466e1f8c..a2f12f6daa 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -3,138 +3,155 @@ namespace PHPStan\Reflection\SignatureMap; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeFunctionReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\TrinaryLogic; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FloatType; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; -use PHPStan\Type\StringType; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_map; +use function str_contains; +use function strtolower; -class NativeFunctionReflectionProvider +#[AutowiredService] +final class NativeFunctionReflectionProvider { /** @var NativeFunctionReflection[] */ - private static array $functionMap = []; - - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - private \PHPStan\BetterReflection\Reflector\FunctionReflector $functionReflector; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private StubPhpDocProvider $stubPhpDocProvider; - - public function __construct(SignatureMapProvider $signatureMapProvider, FunctionReflector $functionReflector, FileTypeMapper $fileTypeMapper, StubPhpDocProvider $stubPhpDocProvider) + private array $functionMap = []; + + public function __construct( + private SignatureMapProvider $signatureMapProvider, + #[AutowiredParameter(ref: '@betterReflectionReflector')] + private Reflector $reflector, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + ) { - $this->signatureMapProvider = $signatureMapProvider; - $this->functionReflector = $functionReflector; - $this->fileTypeMapper = $fileTypeMapper; - $this->stubPhpDocProvider = $stubPhpDocProvider; } public function findFunctionReflection(string $functionName): ?NativeFunctionReflection { $lowerCasedFunctionName = strtolower($functionName); - if (isset(self::$functionMap[$lowerCasedFunctionName])) { - return self::$functionMap[$lowerCasedFunctionName]; + $realFunctionName = $lowerCasedFunctionName; + if (isset($this->functionMap[$lowerCasedFunctionName])) { + return $this->functionMap[$lowerCasedFunctionName]; } if (!$this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName)) { return null; } - $reflectionFunction = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null); - - $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static function (ParameterSignature $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); - - $variants = []; - $i = 0; - while ($this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName, $i)) { - $functionSignature = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null, $i); - $variants[] = new FunctionVariant( - TemplateTypeMap::createEmpty(), - null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterReflection { - $type = $parameterSignature->getType(); - $defaultValue = null; - - $phpDocType = null; - if ($phpDoc !== null) { - $phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null; - if ($phpDocParam !== null) { - $phpDocType = $phpDocParam->getType(); - } - } - if ( - $parameterSignature->getName() === 'values' - && ( - $lowerCasedFunctionName === 'printf' - || $lowerCasedFunctionName === 'sprintf' - ) - ) { - $type = new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]); - } - if ( - $parameterSignature->getName() === 'fields' - && $lowerCasedFunctionName === 'fputcsv' - ) { - $type = new ArrayType( - new UnionType([ - new StringType(), - new IntegerType(), - ]), - new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]) - ); + $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(); + if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) { + return null; + } + $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(); } + } + } + } catch (IdentifierNotFound | InvalidIdentifierName) { + // pass + } - if ( - $lowerCasedFunctionName === 'array_reduce' - && $parameterSignature->getName() === 'initial' - ) { - $defaultValue = new NullType(); - } + $functionSignaturesResult = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); - return new NativeParameterReflection( - $parameterSignature->getName(), - $parameterSignature->isOptional(), - TypehintHelper::decideType($type, $phpDocType), - $parameterSignature->passedByReference(), - $parameterSignature->isVariadic(), - $defaultValue - ); - }, $functionSignature->getParameters()), - $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null) - ); - - $i++; + $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)) { @@ -143,31 +160,20 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $hasSideEffects = TrinaryLogic::createMaybe(); } - $throwType = null; - try { - $reflectionFunction = $this->functionReflector->reflect($functionName); - if ($reflectionFunction->getFileName() !== null) { - $fileName = $reflectionFunction->getFileName(); - $docComment = $reflectionFunction->getDocComment(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); - $throwsTag = $resolvedPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - } - } catch (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { - // pass - } catch (InvalidIdentifierName $e) { - // pass - } - $functionReflection = new NativeFunctionReflection( - $lowerCasedFunctionName, - $variants, + $realFunctionName, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $throwType, - $hasSideEffects + $hasSideEffects, + $isDeprecated, + $asserts, + $docComment, + $returnsByReference, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($attributes, InitializerExprContext::fromFunction($realFunctionName, $fileName)), ); - self::$functionMap[$lowerCasedFunctionName] = $functionReflection; + $this->functionMap[$lowerCasedFunctionName] = $functionReflection; return $functionReflection; } @@ -182,4 +188,15 @@ private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type return $returnTag->getType(); } + private static function getParamOutTypeFromPhpDoc(string $paramName, ResolvedPhpDocBlock $stubPhpDoc): ?Type + { + $paramOutTags = $stubPhpDoc->getParamOutTags(); + + if (array_key_exists($paramName, $paramOutTags)) { + return $paramOutTags[$paramName]->getType(); + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index d4fc818292..7649825119 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -5,36 +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\Type\Type $nativeType; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - public function __construct( - string $name, - bool $optional, - Type $type, - Type $nativeType, - PassedByReference $passedByReference, - bool $variadic + private string $name, + private bool $optional, + private Type $type, + private Type $nativeType, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, + private ?Type $outType, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->nativeType = $nativeType; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; } public function getName(): string @@ -67,4 +51,14 @@ public function isVariadic(): bool return $this->variadic; } + public function getDefaultValue(): ?Type + { + return $this->defaultValue; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index 1c136b8185..61b1553782 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -2,68 +2,81 @@ namespace PHPStan\Reflection\SignatureMap; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\FunctionLike; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Php8StubsMap; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; - -class Php8SignatureMapProvider implements SignatureMapProvider +use ReflectionFunctionAbstract; +use function array_key_exists; +use function array_map; +use function count; +use function explode; +use function is_string; +use function sprintf; +use function strtolower; + +#[AutowiredService(as: Php8SignatureMapProvider::class)] +final class Php8SignatureMapProvider implements SignatureMapProvider { private const DIRECTORY = __DIR__ . '/../../../vendor/phpstan/php-8-stubs'; - private FunctionSignatureMapProvider $functionSignatureMapProvider; - - private FileNodesFetcher $fileNodesFetcher; - - private FileTypeMapper $fileTypeMapper; - /** @var array> */ private array $methodNodes = []; + /** @var array> */ + private array $constantTypes = []; + + private Php8StubsMap $map; + public function __construct( - FunctionSignatureMapProvider $functionSignatureMapProvider, - FileNodesFetcher $fileNodesFetcher, - FileTypeMapper $fileTypeMapper + private FunctionSignatureMapProvider $functionSignatureMapProvider, + private FileNodesFetcher $fileNodesFetcher, + private FileTypeMapper $fileTypeMapper, + private PhpVersion $phpVersion, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProviderProvider $reflectionProviderProvider, ) { - $this->functionSignatureMapProvider = $functionSignatureMapProvider; - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileTypeMapper = $fileTypeMapper; + $this->map = new Php8StubsMap($phpVersion->getVersionId()); } - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool + public function hasMethodSignature(string $className, string $methodName): bool { $lowerClassName = strtolower($className); - if (!array_key_exists($lowerClassName, Php8StubsMap::CLASSES)) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); - } - - if ($variant > 0) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } if ($this->findMethodNode($className, $methodName) === null) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); + return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } return true; } /** - * @param string $className - * @param string $methodName * @return array{ClassMethod, string}|null - * @throws \PHPStan\ShouldNotHappenException */ private function findMethodNode(string $className, string $methodName): ?array { @@ -73,16 +86,16 @@ private function findMethodNode(string $className, string $methodName): ?array return $this->methodNodes[$lowerClassName][$lowerMethodName]; } - $stubFile = self::DIRECTORY . '/' . Php8StubsMap::CLASSES[$lowerClassName]; + $stubFile = self::DIRECTORY . '/' . $this->map->classes[$lowerClassName]; $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); $classes = $nodes->getClassNodes(); if (count($classes) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); } $class = $classes[$lowerClassName]; if (count($class) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); } foreach ($class[0]->getNode()->stmts as $stmt) { @@ -91,6 +104,9 @@ private function findMethodNode(string $className, string $methodName): ?array } if ($stmt->name->toLowerString() === $lowerMethodName) { + if (!$this->isForCurrentVersion($stmt->attrGroups)) { + continue; + } return $this->methodNodes[$lowerClassName][$lowerMethodName] = [$stmt, $stubFile]; } } @@ -98,84 +114,181 @@ private function findMethodNode(string $className, string $methodName): ?array return null; } - public function hasFunctionSignature(string $name, int $variant = 0): bool + /** + * @param AttributeGroup[] $attrGroups + */ + private function isForCurrentVersion(array $attrGroups): bool { - $lowerName = strtolower($name); - if (!array_key_exists($lowerName, Php8StubsMap::FUNCTIONS)) { - return $this->functionSignatureMapProvider->hasFunctionSignature($name, $variant); - } - - if ($variant > 0) { - return $this->functionSignatureMapProvider->hasFunctionSignature($name, $variant); + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === 'Until') { + $arg = $attr->args[0]->value; + if (!$arg instanceof String_) { + throw new ShouldNotHappenException(); + } + $parts = explode('.', $arg->value); + $versionId = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); + if ($this->phpVersion->getVersionId() >= $versionId) { + return false; + } + } + if ($attr->name->toString() !== 'Since') { + continue; + } + + $arg = $attr->args[0]->value; + if (!$arg instanceof String_) { + throw new ShouldNotHappenException(); + } + $parts = explode('.', $arg->value); + $versionId = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); + if ($this->phpVersion->getVersionId() < $versionId) { + return false; + } + } } return true; } - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + public function hasFunctionSignature(string $name): bool { - $lowerClassName = strtolower($className); - if (!array_key_exists($lowerClassName, Php8StubsMap::CLASSES)) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + $lowerName = strtolower($name); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->hasFunctionSignature($name); } - if ($variant > 0) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); - } + return true; + } - if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, 1)) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); } $methodNode = $this->findMethodNode($className, $methodName); if ($methodNode === null) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + return $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); } [$methodNode, $stubFile] = $methodNode; $signature = $this->getSignature($methodNode, $className, $stubFile); if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName)) { - return $this->mergeSignatures( - $signature, - $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant) - ); + $functionMapSignatures = $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); + + return $this->getMergedSignatures($signature, $functionMapSignatures); } - return $signature; + return ['positional' => [$signature], 'named' => null]; } - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature + public function getFunctionSignatures(string $functionName, ?string $className, ReflectionFunctionAbstract|null $reflectionFunction): array { $lowerName = strtolower($functionName); - if (!array_key_exists($lowerName, Php8StubsMap::FUNCTIONS)) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction); } - if ($variant > 0) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + $stubFile = self::DIRECTORY . '/' . $this->map->functions[$lowerName]; + $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); + $functions = $nodes->getFunctionNodes(); + if (!array_key_exists($lowerName, $functions)) { + throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); } + foreach ($functions[$lowerName] as $functionNode) { + if (!$this->isForCurrentVersion($functionNode->getNode()->getAttrGroups())) { + continue; + } + + $signature = $this->getSignature($functionNode->getNode(), null, $stubFile); + if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName)) { + $functionMapSignatures = $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction); - if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName, 1)) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + return $this->getMergedSignatures($signature, $functionMapSignatures); + } + + return ['positional' => [$signature], 'named' => null]; } - $stubFile = self::DIRECTORY . '/' . Php8StubsMap::FUNCTIONS[$lowerName]; - $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); - $functions = $nodes->getFunctionNodes(); - if (count($functions) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + } + + /** + * @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]; } - $signature = $this->getSignature($functions[$lowerName]->getNode(), null, $stubFile); - if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName)) { - return $this->mergeSignatures( - $signature, - $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className) + 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(), ); } - return $signature; + if ($allParamNamesMatchNative || count($namedArgumentsVariants) === 0) { + $namedArgumentsVariants = null; + } + + return ['positional' => $functionMapSignatures['positional'], 'named' => $namedArgumentsVariants]; } private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSignature $functionMapSignature): FunctionSignature @@ -196,12 +309,14 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeParameterType, TypehintHelper::decideType( $nativeParameter->getType(), - $functionMapParameter->getType() - ) + $functionMapParameter->getType(), + ), ), $nativeParameterType, $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), - $nativeParameter->isVariadic() + $nativeParameter->isVariadic(), + $nativeParameter->getDefaultValue(), + $nativeParameter->getOutType(), ); } @@ -213,8 +328,8 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeReturnType, TypehintHelper::decideType( $nativeSignature->getReturnType(), - $functionMapSignature->getReturnType() - ) + $functionMapSignature->getReturnType(), + ), ); } @@ -222,7 +337,7 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $parameters, $returnType, $nativeReturnType, - $nativeSignature->isVariadic() + $nativeSignature->isVariadic(), ); } @@ -237,8 +352,6 @@ public function hasFunctionMetadata(string $name): bool } /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array @@ -247,7 +360,6 @@ public function getMethodMetadata(string $className, string $methodName): array } /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array @@ -255,62 +367,159 @@ public function getFunctionMetadata(string $functionName): array return $this->functionSignatureMapProvider->getFunctionMetadata($functionName); } - /** - * @param ClassMethod|Function_ $function - * @param string $stubFile - * @return FunctionSignature - */ private function getSignature( - FunctionLike $function, + ClassMethod|Function_ $function, ?string $className, - string $stubFile + string $stubFile, ): FunctionSignature { $phpDocParameterTypes = null; $phpDocReturnType = null; if ($function->getDocComment() !== null) { + if ($function instanceof ClassMethod) { + $functionName = $function->name->toString(); + } elseif ($function->namespacedName !== null) { + $functionName = $function->namespacedName->toString(); + } else { + throw new ShouldNotHappenException(); + } $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $stubFile, $className, null, - $function instanceof ClassMethod ? $function->name->toString() : $function->namespacedName->toString(), - $function->getDocComment()->getText() + $functionName, + $function->getDocComment()->getText(), ); - $phpDocParameterTypes = array_map(static function (ParamTag $param): Type { - return $param->getType(); - }, $phpDoc->getParamTags()); + $phpDocParameterTypes = array_map(static fn (ParamTag $param): Type => $param->getType(), $phpDoc->getParamTags()); if ($phpDoc->getReturnTag() !== null) { $phpDocReturnType = $phpDoc->getReturnTag()->getType(); } } + + $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 \PHPStan\ShouldNotHappenException(); + 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); + } + } } - $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); + $parameters[] = new ParameterSignature( $name->name, $param->default !== null || $param->variadic, - TypehintHelper::decideType($parameterType, $phpDocParameterTypes[$name->name] ?? null), + TypehintHelper::decideType($parameterType, $phpDocParameterType), $parameterType, $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), - $param->variadic + $param->variadic, + $param->default !== null ? $this->initializerExprTypeResolver->getType( + $param->default, + InitializerExprContext::fromStubParameter($className, $stubFile, $function), + ) : null, + null, ); $variadic = $variadic || $param->variadic; } - $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), null); + $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), $classReflection); return new FunctionSignature( $parameters, TypehintHelper::decideType($returnType, $phpDocReturnType ?? null), $returnType, - $variadic + $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 166025c41c..fab2c15313 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -2,19 +2,26 @@ namespace PHPStan\Reflection\SignatureMap; +use Nette\Utils\Strings; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class SignatureMapParser { - private \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver; + private TypeStringResolver $typeStringResolver; public function __construct( - TypeStringResolver $typeNodeResolver + TypeStringResolver $typeNodeResolver, ) { $this->typeStringResolver = $typeNodeResolver; @@ -22,8 +29,6 @@ public function __construct( /** * @param mixed[] $map - * @param string|null $className - * @return \PHPStan\Reflection\SignatureMap\FunctionSignature */ public function getFunctionSignature(array $map, ?string $className): FunctionSignature { @@ -39,7 +44,7 @@ public function getFunctionSignature(array $map, ?string $className): FunctionSi $parameterSignatures, $this->getTypeFromString($map[0], $className), new MixedType(), - $hasVariadic + $hasVariadic, ); } @@ -54,7 +59,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type /** * @param array $parameterMap - * @return array + * @return list */ private function getParameters(array $parameterMap): array { @@ -67,7 +72,9 @@ private function getParameters(array $parameterMap): array $this->getTypeFromString($typeString, null), new MixedType(), $passedByReference, - $isVariadic + $isVariadic, + null, + null, ); } @@ -75,29 +82,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 || strpos($reference, '&') === 0) { + } elseif (str_starts_with($reference, '&')) { $passedByReference = PassedByReference::createCreatesNewVariable(); } else { $passedByReference = PassedByReference::createNo(); diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index c1f49dbddb..3999d919b8 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -2,32 +2,44 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Type\Type; +use ReflectionFunctionAbstract; + +#[AutowiredService(factory: '@PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory::create')] interface SignatureMapProvider { - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool; + public function hasMethodSignature(string $className, string $methodName): bool; - public function hasFunctionSignature(string $name, int $variant = 0): bool; + public function hasFunctionSignature(string $name): bool; - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature; + /** @return array{positional: array, named: ?array} */ + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array; - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature; + /** @return array{positional: array, named: ?array} */ + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array; public function hasMethodMetadata(string $className, string $methodName): bool; public function hasFunctionMetadata(string $name): bool; /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array; /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array; + public function hasClassConstantMetadata(string $className, string $constantName): bool; + + /** + * @return array{nativeType: Type} + */ + public function getClassConstantMetadata(string $className, string $constantName): array; + } diff --git a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php index 202e1cb71b..5e2c6a3fb3 100644 --- a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php +++ b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php @@ -2,26 +2,19 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -class SignatureMapProviderFactory +#[AutowiredService] +final class SignatureMapProviderFactory { - private PhpVersion $phpVersion; - - private FunctionSignatureMapProvider $functionSignatureMapProvider; - - private Php8SignatureMapProvider $php8SignatureMapProvider; - public function __construct( - PhpVersion $phpVersion, - FunctionSignatureMapProvider $functionSignatureMapProvider, - Php8SignatureMapProvider $php8SignatureMapProvider + private PhpVersion $phpVersion, + private FunctionSignatureMapProvider $functionSignatureMapProvider, + private Php8SignatureMapProvider $php8SignatureMapProvider, ) { - $this->phpVersion = $phpVersion; - $this->functionSignatureMapProvider = $functionSignatureMapProvider; - $this->php8SignatureMapProvider = $php8SignatureMapProvider; } public function create(): SignatureMapProvider diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index c574046278..2863ff83cd 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -2,16 +2,23 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function sprintf; -/** @api */ -class TrivialParametersAcceptor implements ParametersAcceptor +/** + * @api + */ +final class TrivialParametersAcceptor implements ExtendedParametersAcceptor, CallableParametersAcceptor { /** @api */ - public function __construct() + public function __construct(private string $callableName = 'callable') { } @@ -25,9 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -43,4 +52,55 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 70fae4e624..1fba733364 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -4,46 +4,37 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_map; -class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private MethodReflection $methodReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; /** - * @param MethodReflection $methodReflection - * @param ClassReflection $resolvedDeclaringClass - * @param bool $resolveTemplateTypeMapToBounds * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - MethodReflection $methodReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - callable $transformStaticTypeCallback + private ExtendedMethodReflection $methodReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + callable $transformStaticTypeCallback, ) { - $this->methodReflection = $methodReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; $this->transformStaticTypeCallback = $transformStaticTypeCallback; } @@ -57,25 +48,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype $this->methodReflection, $this->resolvedDeclaringClass, false, - $this->transformStaticTypeCallback + $this->transformStaticTypeCallback, ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -85,32 +78,67 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio $this->methodReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(function (ParametersAcceptor $acceptor): ParametersAcceptor { - return new FunctionVariant( + $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(function (ParameterReflection $parameter): ParameterReflection { - return new DummyParameter( + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), $this->transformStaticType($parameter->getType()), $parameter->isOptional(), $parameter->passedByReference(), $parameter->isVariadic(), - $parameter->getDefaultValue() - ); - }, $acceptor->getParameters()), + $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(), - $this->transformStaticType($acceptor->getReturnType()) + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), ); - }, $method->getVariants()); - - return new ChangedTypeMethodReflection($declaringClass, $method, $variants); + }; + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentVariants = $method->getNamedArgumentsVariants(); + $namedArgumentVariants = $namedArgumentVariants !== null + ? array_map($variantFn, $namedArgumentVariants) + : null; + $throwType = $method->getThrowType(); + $throwType = $throwType !== null + ? $this->transformStaticType($throwType) + : null; + + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentVariants, + $selfOutType, + $throwType, + $method->getAsserts()->mapTypes($this->transformStaticTypeCallback), + ); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php index 903b081c58..06069f8410 100644 --- a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -4,42 +4,30 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\Type; -class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private PropertyReflection $propertyReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; /** - * @param PropertyReflection $propertyReflection - * @param ClassReflection $resolvedDeclaringClass - * @param bool $resolveTemplateTypeMapToBounds * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - PropertyReflection $propertyReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - callable $transformStaticTypeCallback + private ExtendedPropertyReflection $propertyReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + callable $transformStaticTypeCallback, ) { - $this->propertyReflection = $propertyReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; $this->transformStaticTypeCallback = $transformStaticTypeCallback; } @@ -53,25 +41,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy $this->propertyReflection, $this->resolvedDeclaringClass, false, - $this->transformStaticTypeCallback + $this->transformStaticTypeCallback, ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -81,16 +71,18 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect $this->propertyReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 069592d59d..68d3200bec 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -4,42 +4,34 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\StaticType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use function array_map; +use function count; -class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private MethodReflection $methodReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - - private Type $calledOnType; - - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - MethodReflection $methodReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - Type $calledOnType + private ExtendedMethodReflection $methodReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + private Type $calledOnType, ) { - $this->methodReflection = $methodReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; - $this->calledOnType = $calledOnType; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -52,25 +44,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype $this->methodReflection, $this->resolvedDeclaringClass, false, - $this->calledOnType + $this->calledOnType, ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -80,37 +74,81 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio $this->methodReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(function (ParametersAcceptor $acceptor): ParametersAcceptor { - return new FunctionVariant( + $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(function (ParameterReflection $parameter): ParameterReflection { - return new DummyParameter( + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), $this->transformStaticType($parameter->getType()), $parameter->isOptional(), $parameter->passedByReference(), $parameter->isVariadic(), - $parameter->getDefaultValue() - ); - }, $acceptor->getParameters()), + $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(), - $this->transformStaticType($acceptor->getReturnType()) + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), ); - }, $method->getVariants()); - - return new ChangedTypeMethodReflection($declaringClass, $method, $variants); + }; + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentsVariants = $method->getNamedArgumentsVariants(); + $namedArgumentsVariants = $namedArgumentsVariants !== null + ? array_map($variantFn, $namedArgumentsVariants) + : null; + $throwType = $method->getThrowType(); + $throwType = $throwType !== null + ? $this->transformStaticType($throwType) + : null; + + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentsVariants, + $selfOutType, + $throwType, + $method->getAsserts()->mapTypes(fn (Type $type): Type => $this->transformStaticType($type)), + ); } private function transformStaticType(Type $type): Type { return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof GenericStaticType) { + $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); + if (count($calledOnTypeReflections) === 1) { + $calledOnTypeReflection = $calledOnTypeReflections[0]; + + return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType()); + } + + return $this->calledOnType; + } if ($type instanceof StaticType) { return $this->calledOnType; } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index 59bd33d067..18beaf3f8e 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -4,38 +4,26 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private PropertyReflection $propertyReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - - private Type $fetchedOnType; - - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - PropertyReflection $propertyReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - Type $fetchedOnType + private ExtendedPropertyReflection $propertyReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + private Type $fetchedOnType, ) { - $this->propertyReflection = $propertyReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; - $this->fetchedOnType = $fetchedOnType; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -48,25 +36,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy $this->propertyReflection, $this->resolvedDeclaringClass, false, - $this->fetchedOnType + $this->fetchedOnType, ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -76,16 +66,18 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect $this->propertyReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index ad136f3abf..8d471a85b6 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,28 +79,40 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $returnType = TypeCombinator::intersect(...array_map(static function (MethodReflection $method): Type { - return TypeCombinator::intersect(...array_map(static function (ParametersAcceptor $acceptor): Type { - return $acceptor->getReturnType(); - }, $method->getVariants())); - }, $this->methods)); + $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 - ); - }, $this->methods[0]->getVariants()); + 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 @@ -127,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 @@ -161,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 @@ -171,4 +192,45 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); + } + } diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index 9ecb2f7947..b85dfda0d8 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -3,23 +3,29 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; -class IntersectionTypePropertyReflection implements PropertyReflection +final class IntersectionTypePropertyReflection implements ExtendedPropertyReflection { - /** @var PropertyReflection[] */ - private array $properties; - /** - * @param \PHPStan\Reflection\PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ - public function __construct(array $properties) + public function __construct(private array $properties) { - $this->properties = $properties; + } + + public function getName(): string + { + return $this->properties[0]->getName(); } public function getDeclaringClass(): ClassReflection @@ -29,42 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if ($property->isStatic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if (!$property->isPrivate()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if ($property->isPublic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isDeprecated(); - }, $this->properties)); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -91,9 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isInternal(); - }, $this->properties)); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -101,51 +85,131 @@ 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 function (PropertyReflection $property): Type { - return $property->getReadableType(); - }, $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::intersect(...array_map(static function (PropertyReflection $property): Type { - return $property->getWritableType(); - }, $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); + } + + public function isWritable(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(ExtendedPropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = false; foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } + $result = $result || $cb($property); } - return true; + return $result; } - public function isWritable(): bool + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isVirtual()); + } + + public function hasHook(string $hookType): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasHook($hookType)); + } + + public function getHook(string $hookType): ExtendedMethodReflection { + $hooks = []; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; + if (!$property->hasHook($hookType)) { + continue; } + + $hooks[] = $property->getHook($hookType); + } + + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); } - return true; + 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(); + } + + public function isDummy(): TrinaryLogic + { + // uses method typical for unions + // because for this to return yes(), all methods should be dummy + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); } } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php index d8252da113..fe3e09bd4e 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -2,18 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; +use function array_map; -class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private string $methodName; - - /** @var UnresolvedMethodPrototypeReflection[] */ - private array $methodPrototypes; - - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +18,10 @@ class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedM * @param UnresolvedMethodPrototypeReflection[] $methodPrototypes */ public function __construct( - string $methodName, - array $methodPrototypes + private string $methodName, + private array $methodPrototypes, ) { - $this->methodName = $methodName; - $this->methodPrototypes = $methodPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -35,33 +30,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->methodPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } - $methods = array_map(static function (UnresolvedMethodPrototypeReflection $prototype): MethodReflection { - return $prototype->getTransformedMethod(); - }, $this->methodPrototypes); + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); return $this->transformedMethod = new IntersectionTypeMethodReflection($this->methodName, $methods); } public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection { - return new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype) use ($type): UnresolvedMethodPrototypeReflection { - return $prototype->withCalledOnType($type); - }, $this->methodPrototypes)); + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); } } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php index 4be1d50ea0..51e6ccaf46 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,18 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; +use function array_map; -class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private string $propertyName; - - /** @var UnresolvedPropertyPrototypeReflection[] */ - private array $propertyPrototypes; - - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +18,10 @@ class IntersectionTypeUnresolvedPropertyPrototypeReflection implements Unresolve * @param UnresolvedPropertyPrototypeReflection[] $propertyPrototypes */ public function __construct( - string $propertyName, - array $propertyPrototypes + private string $propertyName, + private array $propertyPrototypes, ) { - $this->propertyName = $propertyName; - $this->propertyPrototypes = $propertyPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -35,33 +30,27 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->propertyPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } - $properties = array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection { - return $prototype->getTransformedProperty(); - }, $this->propertyPrototypes); + $properties = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); return $this->transformedProperty = new IntersectionTypePropertyReflection($properties); } public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection { - return new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype) use ($type): UnresolvedPropertyPrototypeReflection { - return $prototype->withFechedOnType($type); - }, $this->propertyPrototypes)); + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); } } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 54ca673a7a..ea6899f7b4 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,29 +78,24 @@ 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 @@ -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,39 @@ 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(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); + } + } diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index 0936fff8d8..a02751d0d4 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -3,23 +3,29 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; -class UnionTypePropertyReflection implements PropertyReflection +final class UnionTypePropertyReflection implements ExtendedPropertyReflection { - /** @var PropertyReflection[] */ - private array $properties; - /** - * @param \PHPStan\Reflection\PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ - public function __construct(array $properties) + public function __construct(private array $properties) { - $this->properties = $properties; + } + + public function getName(): string + { + return $this->properties[0]->getName(); } public function getDeclaringClass(): ClassReflection @@ -29,42 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if (!$property->isStatic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if ($property->isPrivate()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if (!$property->isPublic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isDeprecated(); - }, $this->properties)); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -91,9 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isInternal(); - }, $this->properties)); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -101,51 +85,129 @@ 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 function (PropertyReflection $property): Type { - return $property->getReadableType(); - }, $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::union(...array_map(static function (PropertyReflection $property): Type { - return $property->getWritableType(); - }, $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); + } + + public function isWritable(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(ExtendedPropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = true; foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } + $result = $result && $cb($property); } - return true; + return $result; } - public function isWritable(): bool + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isVirtual()); + } + + public function hasHook(string $hookType): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasHook($hookType)); + } + + public function getHook(string $hookType): ExtendedMethodReflection { + $hooks = []; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; + if (!$property->hasHook($hookType)) { + continue; } + + $hooks[] = $property->getHook($hookType); + } + + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); } - return true; + 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(); + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); } } diff --git a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php index 3d8e295dc7..627a1e5b80 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -2,18 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; +use function array_map; -class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private string $methodName; - - /** @var UnresolvedMethodPrototypeReflection[] */ - private array $methodPrototypes; - - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +18,10 @@ class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPr * @param UnresolvedMethodPrototypeReflection[] $methodPrototypes */ public function __construct( - string $methodName, - array $methodPrototypes + private string $methodName, + private array $methodPrototypes, ) { - $this->methodName = $methodName; - $this->methodPrototypes = $methodPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -35,34 +30,28 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->methodPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } - $methods = array_map(static function (UnresolvedMethodPrototypeReflection $prototype): MethodReflection { - return $prototype->getTransformedMethod(); - }, $this->methodPrototypes); + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); return $this->transformedMethod = new UnionTypeMethodReflection($this->methodName, $methods); } public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection { - return new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype) use ($type): UnresolvedMethodPrototypeReflection { - return $prototype->withCalledOnType($type); - }, $this->methodPrototypes)); + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); } } diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php index ce86e932b2..b28625e3c3 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,18 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; +use function array_map; -class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private string $propertyName; - - /** @var UnresolvedPropertyPrototypeReflection[] */ - private array $propertyPrototypes; - - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +18,10 @@ class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedProper * @param UnresolvedPropertyPrototypeReflection[] $propertyPrototypes */ public function __construct( - string $methodName, - array $propertyPrototypes + private string $propertyName, + private array $propertyPrototypes, ) { - $this->propertyName = $methodName; - $this->propertyPrototypes = $propertyPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -34,34 +29,28 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy if ($this->cachedDoNotResolveTemplateTypeMapToBounds !== null) { return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->propertyPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } - $methods = array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection { - return $prototype->getTransformedProperty(); - }, $this->propertyPrototypes); + $methods = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); return $this->transformedProperty = new UnionTypePropertyReflection($methods); } public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection { - return new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype) use ($type): UnresolvedPropertyPrototypeReflection { - return $prototype->withFechedOnType($type); - }, $this->propertyPrototypes)); + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); } } diff --git a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php index 27f36dfaa3..4665670cb4 100644 --- a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Type\Type; interface UnresolvedMethodPrototypeReflection @@ -10,9 +10,9 @@ interface UnresolvedMethodPrototypeReflection public function doNotResolveTemplateTypeMapToBounds(): self; - public function getNakedMethod(): MethodReflection; + public function getNakedMethod(): ExtendedMethodReflection; - public function getTransformedMethod(): MethodReflection; + public function getTransformedMethod(): ExtendedMethodReflection; public function withCalledOnType(Type $type): self; diff --git a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php index 867f3c1dda..441d4a36c3 100644 --- a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Type\Type; interface UnresolvedPropertyPrototypeReflection @@ -10,9 +10,9 @@ interface UnresolvedPropertyPrototypeReflection public function doNotResolveTemplateTypeMapToBounds(): self; - public function getNakedProperty(): PropertyReflection; + public function getNakedProperty(): ExtendedPropertyReflection; - public function getTransformedProperty(): PropertyReflection; + public function getTransformedProperty(): ExtendedPropertyReflection; public function withFechedOnType(Type $type): self; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php new file mode 100644 index 0000000000..6fb39dd2f4 --- /dev/null +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -0,0 +1,182 @@ +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 []; + } + + public function mustUseReturnValue(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Reflection/WrappedExtendedPropertyReflection.php b/src/Reflection/WrappedExtendedPropertyReflection.php new file mode 100644 index 0000000000..c2e946c558 --- /dev/null +++ b/src/Reflection/WrappedExtendedPropertyReflection.php @@ -0,0 +1,157 @@ +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 isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Reflection/WrapperPropertyReflection.php b/src/Reflection/WrapperPropertyReflection.php index c34a3c7cfb..e7bb397ace 100644 --- a/src/Reflection/WrapperPropertyReflection.php +++ b/src/Reflection/WrapperPropertyReflection.php @@ -2,9 +2,9 @@ namespace PHPStan\Reflection; -interface WrapperPropertyReflection extends PropertyReflection +interface WrapperPropertyReflection extends ExtendedPropertyReflection { - public function getOriginalReflection(): PropertyReflection; + public function getOriginalReflection(): ExtendedPropertyReflection; } diff --git a/src/Rules/Api/ApiClassConstFetchRule.php b/src/Rules/Api/ApiClassConstFetchRule.php new file mode 100644 index 0000000000..785c29d64f --- /dev/null +++ b/src/Rules/Api/ApiClassConstFetchRule.php @@ -0,0 +1,91 @@ + + */ +#[RegisteredRule(level: 0)] +final class ApiClassConstFetchRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + if (!$node->class instanceof Node\Name) { + return []; + } + + $className = $scope->resolveName($node->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Accessing %s::%s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + $classReflection->getDisplayName(), + $node->name->toString(), + ))->identifier('phpstanApi.classConstant')->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); + + $docBlock = $classReflection->getResolvedPhpDoc(); + if ($docBlock !== null) { + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return []; + } + } + } + + if ($node->name->toLowerString() === 'class') { + foreach ($classReflection->getNativeReflection()->getMethods() as $methodReflection) { + $methodDocComment = $methodReflection->getDocComment(); + if ($methodDocComment === false) { + continue; + } + + if (!str_contains($methodDocComment, '@api')) { + continue; + } + + return []; + } + } + + return [$ruleError]; + } + +} diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php index 9ba41ab721..4a2e7c0942 100644 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -6,27 +6,25 @@ use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule */ -class ApiClassExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiClassExtendsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -46,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array } $extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName())) { return []; } @@ -56,10 +54,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', - $extendedClassReflection->getDisplayName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); $docBlock = $extendedClassReflection->getResolvedPhpDoc(); diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php index c0d61f1fc6..c2bdf22c3e 100644 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -5,29 +5,28 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; +use function array_merge; +use function count; +use function in_array; +use function sprintf; /** * @implements Rule */ -class ApiClassImplementsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiClassImplementsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -46,8 +45,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Node\Name $name - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -57,19 +55,19 @@ private function checkName(Scope $scope, Node\Name $name): array } $implementedClassReflection = $this->reflectionProvider->getClass($implementedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $implementedClassReflection->getName(), $implementedClassReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $implementedClassReflection->getName(), $implementedClassReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $implementedClassReflection->getDisplayName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if ($implementedClassReflection->getName() === Type::class) { + if (in_array($implementedClassReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInstanceofRule.php b/src/Rules/Api/ApiInstanceofRule.php new file mode 100644 index 0000000000..13acf1d05c --- /dev/null +++ b/src/Rules/Api/ApiInstanceofRule.php @@ -0,0 +1,120 @@ + + */ +#[RegisteredRule(level: 0)] +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(), classReflection: $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..c88c5114bf --- /dev/null +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -0,0 +1,159 @@ + + */ +#[RegisteredRule(level: 0)] +final class ApiInstanceofTypeRule implements Rule +{ + + private const MAP = [ + TypeWithClassName::class => 'Type::getObjectClassNames() or Type::getObjectClassReflections()', + EnumCaseObjectType::class => 'Type::getEnumCases()', + ConstantArrayType::class => 'Type::getConstantArrays()', + ArrayType::class => 'Type::isArray() or Type::getArrays()', + ConstantStringType::class => 'Type::getConstantStrings()', + StringType::class => 'Type::isString()', + ClassStringType::class => 'Type::isClassStringType()', + IntegerType::class => 'Type::isInteger()', + FloatType::class => 'Type::isFloat()', + NullType::class => 'Type::isNull()', + VoidType::class => 'Type::isVoid()', + BooleanType::class => 'Type::isBoolean()', + ConstantBooleanType::class => 'Type::isTrue() or Type::isFalse()', + CallableType::class => 'Type::isCallable() and Type::getCallableParametersAcceptors()', + IterableType::class => 'Type::isIterable()', + ObjectWithoutClassType::class => 'Type::isObject()', + ObjectType::class => 'Type::isObject() or Type::getObjectClassNames()', + GenericClassStringType::class => 'Type::isClassStringType() and Type::getClassStringObjectType()', + GenericObjectType::class => null, + IntersectionType::class => null, + ConstantScalarType::class => 'Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues()', + ObjectShapeType::class => 'Type::isObject() and Type::hasProperty()', + + // accessory types + NonEmptyArrayType::class => 'Type::isIterableAtLeastOnce()', + OversizedArrayType::class => 'Type::isOversizedArray()', + AccessoryArrayListType::class => 'Type::isList()', + AccessoryNumericStringType::class => 'Type::isNumericString()', + AccessoryLiteralStringType::class => 'Type::isLiteralString()', + AccessoryLowercaseStringType::class => 'Type::isLowercaseString()', + AccessoryUppercaseStringType::class => 'Type::isUppercaseString()', + AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', + AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', + HasMethodType::class => 'Type::hasMethod()', + HasPropertyType::class => 'Type::hasProperty()', + HasOffsetType::class => 'Type::hasOffsetValueType()', + AccessoryType::class => 'methods on PHPStan\\Type\\Type', + ]; + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Instanceof_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + if ($node->getAttribute(TypeTraverserInstanceofVisitor::ATTRIBUTE_NAME, false) === true) { + return []; + } + + $lowerMap = []; + foreach (self::MAP as $className => $method) { + $lowerMap[strtolower($className)] = $method; + } + + $className = $scope->resolveName($node->class); + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $lowerMap)) { + return []; + } + + if ($this->reflectionProvider->hasClass($className)) { + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->is(AccessoryType::class)) { + if ($className === $classReflection->getName()) { + return []; + } + } + } + + $tip = 'Learn more: https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated'; + if ($lowerMap[$lowerClassName] === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated.', + $className, + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated. Use %s instead.', + $className, + $lowerMap[$lowerClassName], + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + +} diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index 136c3b6cff..2939c8e6be 100644 --- a/src/Rules/Api/ApiInstantiationRule.php +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -4,27 +4,25 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function str_contains; /** * @implements Rule */ -class ApiInstantiationRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInstantiationRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -44,16 +42,16 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $this->reflectionProvider->getClass($className); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Creating new %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', - $classReflection->getDisplayName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); if (!$classReflection->hasConstructor()) { @@ -66,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return [$ruleError]; } - if (strpos($docComment, '@api') === false) { + if (!str_contains($docComment, '@api')) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php index 326426b818..6ea4db2d63 100644 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -5,29 +5,28 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Interface_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; +use function array_merge; +use function count; +use function in_array; +use function sprintf; /** * @implements Rule */ -class ApiInterfaceExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInterfaceExtendsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -46,9 +45,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope - * @param Node\Name $name - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -58,19 +55,19 @@ private function checkName(Scope $scope, Node\Name $name): array } $extendedInterfaceReflection = $this->reflectionProvider->getClass($extendedInterface); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedInterfaceReflection->getName(), $extendedInterfaceReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedInterfaceReflection->getName(), $extendedInterfaceReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $extendedInterfaceReflection->getDisplayName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if ($extendedInterfaceReflection->getName() === Type::class) { + if (in_array($extendedInterfaceReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 53db70e53d..87b9bc5f4e 100644 --- a/src/Rules/Api/ApiMethodCallRule.php +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -4,21 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; +use function str_contains; /** * @implements Rule */ -class ApiMethodCallRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiMethodCallRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - public function __construct(ApiRuleHelper $apiRuleHelper) + public function __construct(private ApiRuleHelper $apiRuleHelper) { - $this->apiRuleHelper = $apiRuleHelper; } public function getNodeType(): string @@ -38,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array } $declaringClass = $methodReflection->getDeclaringClass(); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { return []; } @@ -49,10 +51,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), - $methodReflection->getName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); return [$ruleError]; @@ -76,7 +78,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php index 50254219aa..01801a49aa 100644 --- a/src/Rules/Api/ApiRuleHelper.php +++ b/src/Rules/Api/ApiRuleHelper.php @@ -3,10 +3,17 @@ namespace PHPStan\Rules\Api; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\ParentDirectoryRelativePathHelper; +use function dirname; +use function pathinfo; +use function str_starts_with; +use function stripos; +use function strtolower; use const PATHINFO_BASENAME; -class ApiRuleHelper +#[AutowiredService] +final class ApiRuleHelper { public function isPhpStanCode(Scope $scope, string $namespace, ?string $declaringFile): bool @@ -41,7 +48,6 @@ public function isPhpStanCode(Scope $scope, string $namespace, ?string $declarin } /** - * @param string $currentDirectory * @param string[] $parts * @return string[] */ @@ -68,11 +74,11 @@ public function isPhpStanName(string $namespace): bool return true; } - if (strpos($namespace, 'PHPStan\\PhpDocParser\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\PhpDocParser\\')) { return false; } - if (strpos($namespace, 'PHPStan\\BetterReflection\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\BetterReflection\\')) { return false; } diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php index fff1910264..5441c16572 100644 --- a/src/Rules/Api/ApiStaticCallRule.php +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -4,28 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; +use function str_contains; /** * @implements Rule */ -class ApiStaticCallRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiStaticCallRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -56,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection = $classReflection->getNativeMethod($methodName); $declaringClass = $methodReflection->getDeclaringClass(); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { return []; } @@ -67,10 +66,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), - $methodReflection->getName() - ))->tip(sprintf( + $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' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); return [$ruleError]; @@ -94,7 +93,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php index ec616163e5..796d745bab 100644 --- a/src/Rules/Api/ApiTraitUseRule.php +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -4,27 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class ApiTraitUseRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiTraitUseRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -37,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); foreach ($node->traits as $traitName) { $traitName = $traitName->toString(); @@ -46,14 +43,14 @@ public function processNode(Node $node, Scope $scope): array } $traitReflection = $this->reflectionProvider->getClass($traitName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $traitReflection->getName(), $traitReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $traitReflection->getName(), $traitReflection->getFileName())) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', - $traitReflection->getDisplayName() - ))->tip($tip)->build(); + $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 @@ + + */ +#[RegisteredRule(level: 0)] +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..135a97465f --- /dev/null +++ b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php @@ -0,0 +1,81 @@ + + */ +#[RegisteredRule(level: 0)] +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..8d36cc0f9a --- /dev/null +++ b/src/Rules/Api/OldPhpParser4ClassRule.php @@ -0,0 +1,81 @@ + + */ +#[RegisteredRule(level: 0)] +final class OldPhpParser4ClassRule implements Rule +{ + + private const NAME_MAPPING = [ + // from https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md#renamed-nodes + 'PhpParser\Node\Scalar\LNumber' => Node\Scalar\Int_::class, + 'PhpParser\Node\Scalar\DNumber' => Node\Scalar\Float_::class, + 'PhpParser\Node\Scalar\Encapsed' => Node\Scalar\InterpolatedString::class, + 'PhpParser\Node\Scalar\EncapsedStringPart' => Node\InterpolatedStringPart::class, + 'PhpParser\Node\Expr\ArrayItem' => Node\ArrayItem::class, + 'PhpParser\Node\Expr\ClosureUse' => Node\ClosureUse::class, + 'PhpParser\Node\Stmt\DeclareDeclare' => Node\DeclareItem::class, + 'PhpParser\Node\Stmt\PropertyProperty' => Node\PropertyItem::class, + 'PhpParser\Node\Stmt\StaticVar' => Node\StaticVar::class, + 'PhpParser\Node\Stmt\UseUse' => Node\UseItem::class, + ]; + + public function getNodeType(): string + { + return Name::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nameMapping = array_change_key_case(self::NAME_MAPPING); + $lowerName = $node->toLowerString(); + if (!array_key_exists($lowerName, $nameMapping)) { + return []; + } + + $newName = $nameMapping[$lowerName]; + + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Class %s not found. It has been renamed to %s in PHP-Parser v5.', + $node->toString(), + $newName, + ))->identifier('phpParser.classRenamed') + ->build(), + ]; + } + +} diff --git a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php index 81f4ac4c54..bdfc145f9a 100644 --- a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -3,23 +3,28 @@ namespace PHPStan\Rules\Api; use Nette\Utils\Json; +use Nette\Utils\JsonException; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function dirname; +use function is_dir; +use function is_file; +use function str_starts_with; /** * @implements Rule */ -class PhpStanNamespaceIn3rdPartyPackageRule implements Rule +#[RegisteredRule(level: 0)] +final class PhpStanNamespaceIn3rdPartyPackageRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - public function __construct(ApiRuleHelper $apiRuleHelper) + public function __construct(private ApiRuleHelper $apiRuleHelper) { - $this->apiRuleHelper = $apiRuleHelper; } public function getNodeType(): string @@ -43,12 +48,13 @@ public function processNode(Node $node, Scope $scope): array } $packageName = $composerJson['name'] ?? null; - if ($packageName !== null && strpos($packageName, 'phpstan/') === 0) { + if ($packageName !== null && str_starts_with($packageName, 'phpstan/')) { return []; } return [ RuleErrorBuilder::message('Declaring PHPStan namespace is not allowed in 3rd party packages.') + ->identifier('phpstanApi.phpstanNamespace') ->tip("See:\n https://phpstan.org/developing-extensions/backward-compatibility-promise") ->build(), ]; @@ -65,14 +71,19 @@ private function findComposerJsonContents(string $fromDirectory): ?array $composerJsonPath = $fromDirectory . '/composer.json'; if (!is_file($composerJsonPath)) { - return $this->findComposerJsonContents(dirname($fromDirectory)); + $dirName = dirname($fromDirectory); + if ($dirName !== $fromDirectory) { + return $this->findComposerJsonContents($dirName); + } + + return null; } try { return Json::decode(FileReader::read($composerJsonPath), Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { + } catch (JsonException) { return null; - } catch (\PHPStan\File\CouldNotReadFileException $e) { + } catch (CouldNotReadFileException) { return null; } } diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php new file mode 100644 index 0000000000..b46f7c7b36 --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionFunctionRule.php @@ -0,0 +1,78 @@ + + */ +#[RegisteredRule(level: 0)] +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..ab3ae254fc --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionInstantiationRule.php @@ -0,0 +1,97 @@ + + */ +#[RegisteredRule(level: 0)] +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..7d920e167c 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,59 @@ 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); + } elseif ($varType->isList()->yes()) { + return TypeCombinator::intersect( + $varIterableKeyType, + $keyType, + ); + } + + return new MixedType( + subtractedType: 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 246a31b25c..0000000000 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ /dev/null @@ -1,92 +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 - && !$node instanceof AssignRef - ) { - 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 || $node instanceof AssignRef) { - $assignedValueType = $scope->getType($node->expr); - } else { - $assignedValueType = $scope->getType($node); - } - - $itemType = $assignedToType->getItemType(); - if (!$this->ruleLevelHelper->accepts($itemType, $assignedValueType, $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType); - return [ - RuleErrorBuilder::message(sprintf( - 'Array (%s) does not accept %s.', - $assignedToType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel) - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php deleted file mode 100644 index 792e3b436b..0000000000 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ /dev/null @@ -1,92 +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()) { - $verbosity = VerbosityLevel::getRecommendedLevelByType($arrayType->getIterableKeyType(), $keyType); - return [ - RuleErrorBuilder::message(sprintf( - 'Array (%s) does not accept key %s.', - $arrayType->describe($verbosity), - $keyType->describe(VerbosityLevel::value()) - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index de523a58c3..eac1a12956 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -2,38 +2,37 @@ namespace PHPStan\Rules\Arrays; +use ArrayAccess; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * @implements Rule */ -class ArrayDestructuringRule implements Rule +#[RegisteredRule(level: 3)] +final class ArrayDestructuringRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck; } public function getNodeType(): string @@ -43,38 +42,37 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) { + if (!$node->var instanceof Node\Expr\List_) { return []; } return $this->getErrors( $scope, $node->var, - $node->expr + $node->expr, ); } /** - * @param Node\Expr\List_|Node\Expr\Array_ $var - * @return RuleError[] + * @return list */ - private function getErrors(Scope $scope, Expr $var, Expr $expr): array + private function getErrors(Scope $scope, Node\Expr\List_ $var, Expr $expr): array { $exprTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $expr, '', - static function (Type $varType): bool { - return $varType->isArray()->yes(); - } + static fn (Type $varType): bool => $varType->isArray()->yes() || (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes(), ); $exprType = $exprTypeResult->getType(); if ($exprType instanceof ErrorType) { return []; } - if (!$exprType->isArray()->yes()) { + if (!$exprType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($exprType)->yes()) { return [ - RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly()))) + ->identifier('offsetAccess.nonArray') + ->build(), ]; } @@ -89,30 +87,21 @@ static function (Type $varType): bool { $keyExpr = null; if ($item->key === null) { $keyType = new ConstantIntegerType($i); - $keyExpr = new Node\Scalar\LNumber($i); + $keyExpr = new Node\Scalar\Int_($i); } else { $keyType = $scope->getType($item->key); - if ($keyType instanceof ConstantIntegerType) { - $keyExpr = new LNumber($keyType->getValue()); - } elseif ($keyType instanceof ConstantStringType) { - $keyExpr = new Node\Scalar\String_($keyType->getValue()); - } + $keyExpr = new TypeExpr($keyType); } $itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check( $scope, $expr, '', - $keyType + $keyType, ); $errors = array_merge($errors, $itemErrors); - if ($keyExpr === null) { - $i++; - continue; - } - - if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) { + if (!$item->value instanceof Node\Expr\List_) { $i++; continue; } @@ -120,7 +109,7 @@ static function (Type $varType): bool { $errors = array_merge($errors, $this->getErrors( $scope, $item->value, - new Expr\ArrayDimFetch($expr, $keyExpr) + new Expr\ArrayDimFetch($expr, $keyExpr), )); } diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php new file mode 100644 index 0000000000..4dc25092a9 --- /dev/null +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -0,0 +1,67 @@ + + */ +#[RegisteredRule(level: 3)] +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..2251a5e275 100644 --- a/src/Rules/Arrays/DeadForeachRule.php +++ b/src/Rules/Arrays/DeadForeachRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Foreach_> + * @implements Rule */ -class DeadForeachRule implements Rule +#[RegisteredRule(level: 4)] +final class DeadForeachRule implements Rule { public function getNodeType(): string @@ -30,7 +32,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 72e17c0525..c05440f892 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -2,24 +2,35 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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_key_first; +use function array_keys; +use function array_search; +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 +#[RegisteredRule(level: 0)] +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,44 +38,111 @@ 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; + $seenKeys = []; + $seenUnions = []; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); if ($item === null) { - continue; - } - if ($item->key === null) { + $autoGeneratedIndex = false; continue; } $key = $item->key; - $keyType = $itemNode->getScope()->getType($key); - if ( - !$keyType instanceof ConstantScalarType - ) { + if ($key === null) { + if ($autoGeneratedIndex === false) { + continue; + } + + if ($autoGeneratedIndex === null) { + $autoGeneratedIndex = 0; + $keyType = new ConstantIntegerType(0); + } else { + $keyType = new ConstantIntegerType(++$autoGeneratedIndex); + } + } else { + $keyType = $itemNode->getScope()->getType($key); + $arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues(); + if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) { + $autoGeneratedIndex = $autoGeneratedIndex === null + ? $arrayKeyValues[0] + : max($autoGeneratedIndex, $arrayKeyValues[0]); + } + } + + $keyValues = $keyType->toArrayKey()->getConstantScalarValues(); + if (count($keyValues) === 0) { + $autoGeneratedIndex = false; continue; } - $printedValue = $this->printer->prettyPrintExpr($key); - $value = $keyType->getValue(); - $printedValues[$value][] = $printedValue; + $duplicate = false; + $newValues = $keyValues; + foreach ($newValues as $k => $newValue) { + if (array_search($newValue, $seenKeys, true) !== false) { + unset($newValues[$k]); + } - if (!isset($valueLines[$value])) { - $valueLines[$value] = $item->getLine(); + if ($newValues === []) { + $duplicate = true; + break; + } } - $previousCount = count($values); - $values[$value] = $printedValue; - if ($previousCount !== count($values)) { - continue; + if ($newValues !== []) { + if (count($newValues) === 1) { + $newValue = $newValues[array_key_first($newValues)]; + foreach ($seenUnions as $k => $union) { + $offset = array_search($newValue, $union, true); + if ($offset === false) { + continue; + } + + unset($seenUnions[$k][$offset]); + + // turn a union into a seen key, when all its elements have been seen + if (count($seenUnions[$k]) !== 1) { + continue; + } + + $seenKeys[] = $seenUnions[$k][array_key_first($seenUnions[$k])]; + unset($seenUnions[$k]); + } + $seenKeys[] = $newValue; + } else { + $seenUnions[] = $newValues; + } } - $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(); + } + + if (!$duplicate) { + continue; + } + + $duplicateKeys[$value] = true; + } } $messages = []; @@ -74,8 +152,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/EmptyArrayItemRule.php b/src/Rules/Arrays/EmptyArrayItemRule.php deleted file mode 100644 index fa55e6a1f9..0000000000 --- a/src/Rules/Arrays/EmptyArrayItemRule.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -class EmptyArrayItemRule implements Rule -{ - - public function getNodeType(): string - { - return LiteralArrayNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - foreach ($node->getItemNodes() as $itemNode) { - $item = $itemNode->getArrayItem(); - if ($item !== null) { - continue; - } - - return [ - RuleErrorBuilder::message('Literal array contains empty item.') - ->nonIgnorable() - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 18b771b859..5ba1a88009 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -2,58 +2,84 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\MixedType; -use PHPStan\Type\TypeUtils; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Type\ErrorType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class InvalidKeyInArrayDimFetchRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +final class InvalidKeyInArrayDimFetchRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] + 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) { + $varType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '', + static fn (Type $varType): bool => $varType->isArray()->no(), + )->getType(); + + if ($varType instanceof ErrorType) { + return []; + } + + $isArray = $varType->isArray(); + if ($isArray->no() || ($isArray->maybe() && !$this->reportMaybes)) { + return []; + } + + $dimensionType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->dim, + '', + static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimType)->yes(), + )->getType(); + if ($dimensionType instanceof ErrorType) { 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.', + $isArray->yes() && $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..993a6dde61 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -2,30 +2,36 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +final class InvalidKeyInArrayItemRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct( + #[AutowiredParameter] + 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 +42,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..2e4204b35b 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -4,39 +4,40 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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 +50,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 index f9e5c2f32f..c46a62264e 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -3,102 +3,119 @@ namespace PHPStan\Rules\Arrays; use PhpParser\Node\Expr; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\BenevolentUnionType; 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 sprintf; -class NonexistentOffsetInArrayDimFetchCheck +#[AutowiredService] +final class NonexistentOffsetInArrayDimFetchCheck { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - - public function __construct(RuleLevelHelper $ruleLevelHelper, bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] + private bool $reportMaybes, + #[AutowiredParameter] + private bool $reportPossiblyNonexistentGeneralArrayOffset, + #[AutowiredParameter] + private bool $reportPossiblyNonexistentConstantArrayOffset, + ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } /** - * @param Scope $scope - * @param Expr $var - * @param string $unknownClassPattern - * @param Type $dimType - * @return RuleError[] + * @return list */ public function check( Scope $scope, Expr $var, string $unknownClassPattern, - Type $dimType + Type $dimType, ): array { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var), $unknownClassPattern, - static function (Type $type) use ($dimType): bool { - return $type->hasOffsetValueType($dimType)->yes(); - } + static fn (Type $type): bool => $type->hasOffsetValueType($dimType)->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { return $typeResult->getUnknownClassErrors(); } - $hasOffsetValueType = $type->hasOffsetValueType($dimType); - $report = $hasOffsetValueType->no(); - if ($hasOffsetValueType->maybe()) { - $constantArrays = TypeUtils::getOldConstantArrays($type); - if (count($constantArrays) > 0) { - foreach ($constantArrays as $constantArray) { - if ($constantArray->hasOffsetValueType($dimType)->no()) { - $report = true; - break; - } - } - } + if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) { + return []; + } + + if ($type->hasOffsetValueType($dimType)->no()) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - if (!$report && $this->reportMaybes) { + if ($this->reportMaybes) { + $report = false; + if ($type instanceof BenevolentUnionType) { $flattenedTypes = [$type]; } else { $flattenedTypes = TypeUtils::flattenTypes($type); } + foreach ($flattenedTypes as $innerType) { - if ($dimType instanceof UnionType) { - if ($innerType->hasOffsetValueType($dimType)->no()) { - $report = true; - break; - } - continue; + 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; } - foreach (TypeUtils::flattenTypes($dimType) as $innerDimType) { - if ($innerType->hasOffsetValueType($innerDimType)->no()) { + if ($dimType instanceof BenevolentUnionType) { + $flattenedInnerTypes = [$dimType]; + } else { + $flattenedInnerTypes = TypeUtils::flattenTypes($dimType); + } + foreach ($flattenedInnerTypes as $innerDimType) { + if ( + $innerType->hasOffsetValueType($innerDimType)->no() + ) { $report = true; - break; + break 2; } } } - } - if ($report) { - if ($scope->isInExpressionAssign($var)) { - return []; + if ($report) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s might not exist on %s.', $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - - return [ - RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(), - ]; } return []; diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 758d76cb43..f624f1a097 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -2,44 +2,53 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\Variable; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function count; +use function in_array; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class NonexistentOffsetInArrayDimFetchRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +final class NonexistentOffsetInArrayDimFetchRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, + #[AutowiredParameter] + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { + if ($this->isImplicitArrayCreation($node, $scope)->yes()) { + return []; + } + if ($node->dim !== null) { $dimType = $scope->getType($node->dim); $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value()))); @@ -50,40 +59,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(), ]; } @@ -94,12 +109,73 @@ static function (Type $type): bool { return []; } + if ( + $node->dim instanceof Node\Expr\FuncCall + && $node->dim->name instanceof Node\Name + && in_array($node->dim->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($node->dim->getArgs()) >= 1 + ) { + $arrayArg = $node->dim->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + + if ( + $node->dim instanceof Node\Expr\BinaryOp\Minus + && $node->dim->left instanceof Node\Expr\FuncCall + && $node->dim->left->name instanceof Node\Name + && in_array($node->dim->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($node->dim->left->getArgs()) >= 1 + && $node->dim->right instanceof Node\Scalar\Int_ + && $node->dim->right->value === 1 + ) { + $arrayArg = $node->dim->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + return $this->nonexistentOffsetInArrayDimFetchCheck->check( $scope, $node->var, $unknownClassPattern, - $dimType + $dimType, ); } + private function isImplicitArrayCreation(Node\Expr\ArrayDimFetch $node, Scope $scope): TrinaryLogic + { + $varNode = $node->var; + while ($varNode instanceof ArrayDimFetch) { + $varNode = $varNode->var; + } + + if (!$varNode instanceof Variable) { + return TrinaryLogic::createNo(); + } + + if (!is_string($varNode->name)) { + return TrinaryLogic::createNo(); + } + + return $scope->hasVariableType($varNode->name)->negate(); + } + } diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index 054d988fbd..466d5cd5c8 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -2,34 +2,36 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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 +51,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 +63,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 +82,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 +91,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..838636feb6 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -2,33 +2,36 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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 +44,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 +67,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 +83,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 +92,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 ea571f6499..3d21a95970 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -2,28 +2,30 @@ 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\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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,7 +33,7 @@ public function getNodeType(): string return Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Assign @@ -46,6 +48,10 @@ 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 || $node instanceof Expr\AssignRef) { $assignedValueType = $scope->getType($node->expr); @@ -53,7 +59,6 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - $originalArrayType = $scope->getType($arrayDimFetch->var); $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, @@ -61,7 +66,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) { @@ -76,12 +81,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 cfc5fdb66c..b48a5b8966 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php @@ -2,21 +2,25 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class OffsetAccessWithoutDimForReadingRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +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 +31,10 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Cannot use [] for reading.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Cannot use [] for reading.') + ->identifier('offsetAccess.noDim') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Arrays/UnpackIterableInArrayRule.php b/src/Rules/Arrays/UnpackIterableInArrayRule.php index a061c670a2..0e7907b59d 100644 --- a/src/Rules/Arrays/UnpackIterableInArrayRule.php +++ b/src/Rules/Arrays/UnpackIterableInArrayRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\LiteralArrayNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,20 +12,19 @@ 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 +#[RegisteredRule(level: 3)] +final class UnpackIterableInArrayRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -48,9 +48,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $item->value, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -63,8 +61,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 index 205e3994d4..36b3c6faa6 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -2,43 +2,44 @@ namespace PHPStan\Rules; +use Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use function array_key_exists; +use function count; +use function sprintf; +use function strtolower; -class AttributesCheck +#[AutowiredService] +final class AttributesCheck { - private ReflectionProvider $reflectionProvider; - - private FunctionCallParametersCheck $functionCallParametersCheck; - - private ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $functionCallParametersCheck, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $functionCallParametersCheck, + private ClassNameCheck $classCheck, + #[AutowiredParameter] + private bool $deprecationRulesInstalled, ) { - $this->reflectionProvider = $reflectionProvider; - $this->functionCallParametersCheck = $functionCallParametersCheck; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } /** * @param AttributeGroup[] $attrGroups - * @param \Attribute::TARGET_* $requiredTarget - * @return RuleError[] + * @param int-mask-of $requiredTarget + * @return list */ public function check( Scope $scope, array $attrGroups, int $requiredTarget, - string $targetName + string $targetName, ): array { $errors = []; @@ -47,72 +48,115 @@ public function check( foreach ($attrGroup->attrs as $attribute) { $name = $attribute->name->toString(); if (!$this->reflectionProvider->hasClass($name)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name)) + ->line($attribute->getStartLine()) + ->identifier('attribute.notFound') + ->build(); continue; } $attributeClass = $this->reflectionProvider->getClass($name); if (!$attributeClass->isAttributeClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('Class %s is not an Attribute class.', $attributeClass->getDisplayName()))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $attributeClass->getClassTypeDescription(), $attributeClass->getDisplayName())) + ->identifier('attribute.notAttribute') + ->line($attribute->getStartLine()) + ->build(); continue; } if ($attributeClass->isAbstract()) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name)) + ->identifier('attribute.abstract') + ->line($attribute->getStartLine()) + ->build(); } - foreach ($this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames($scope, [new ClassNameNodePair($name, $attribute)], ClassNameUsageLocation::from(ClassNameUsageLocation::ATTRIBUTE)) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } $flags = $attributeClass->getAttributeClassFlags(); if (($flags & $requiredTarget) === 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->build(); } - if (($flags & \Attribute::IS_REPEATABLE) === 0) { + if (($flags & Attribute::IS_REPEATABLE) === 0) { $loweredName = strtolower($name); if (array_key_exists($loweredName, $alreadyPresent)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName)) + ->identifier('attribute.nonRepeatable') + ->line($attribute->getStartLine()) + ->build(); } $alreadyPresent[$loweredName] = true; } + if ($this->deprecationRulesInstalled && $attributeClass->isDeprecated()) { + if ($attributeClass->getDeprecatedDescription() !== null) { + $deprecatedError = sprintf('Attribute class %s is deprecated: %s', $name, $attributeClass->getDeprecatedDescription()); + } else { + $deprecatedError = sprintf('Attribute class %s is deprecated.', $name); + } + $errors[] = RuleErrorBuilder::message($deprecatedError) + ->identifier('attribute.deprecated') + ->line($attribute->getStartLine()) + ->build(); + } + if (!$attributeClass->hasConstructor()) { if (count($attribute->args) > 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name)) + ->identifier('attribute.noConstructor') + ->line($attribute->getStartLine()) + ->build(); } continue; } $attributeConstructor = $attributeClass->getConstructor(); if (!$attributeConstructor->isPublic()) { - $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name)) + ->identifier('attribute.constructorNotPublic') + ->line($attribute->getStartLine()) + ->build(); } $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); + $nodeAttributes = $attribute->getAttributes(); + $nodeAttributes['isAttribute'] = true; + $parameterErrors = $this->functionCallParametersCheck->check( - ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()), + ParametersAcceptorSelector::selectFromArgs( + $scope, + $attribute->args, + $attributeConstructor->getVariants(), + $attributeConstructor->getNamedArgumentsVariants(), + ), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), - new New_($attribute->name, $attribute->args, $attribute->getAttributes()), - [ - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', - '', // constructor does not have a return type - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, - 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', - 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', - 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', - ] + 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) { diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index b0ede225b3..4844d1e0a0 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -4,24 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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\Echo_> + * @implements Rule */ -class EchoRule implements Rule +#[RegisteredRule(level: 2)] +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 +38,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 +50,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..38b97e396a 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -4,48 +4,49 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 +57,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 +93,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..d8d899e72d 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -4,41 +4,40 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 2)] +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 +45,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 +58,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..8e881f8ba3 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -4,24 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Print_> + * @implements Rule */ -class PrintRule implements Rule +#[RegisteredRule(level: 2)] +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 +35,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 +43,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 index c50aca654a..d20722b783 100644 --- a/src/Rules/Cast/UnsetCastRule.php +++ b/src/Rules/Cast/UnsetCastRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,14 +12,12 @@ /** * @implements Rule */ -class UnsetCastRule implements Rule +#[RegisteredRule(level: 0)] +final class UnsetCastRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -33,7 +32,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.') + ->identifier('cast.unset') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Cast/VoidCastRule.php b/src/Rules/Cast/VoidCastRule.php new file mode 100644 index 0000000000..9e01ab6c83 --- /dev/null +++ b/src/Rules/Cast/VoidCastRule.php @@ -0,0 +1,41 @@ + + */ +#[RegisteredRule(level: 0)] +final class VoidCastRule implements Rule +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Node\Expr\Cast\Void_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($scope->isInFirstLevelStatement()) { + return []; + } + + return [ + RuleErrorBuilder::message('The (void) cast cannot be used within an expression.') + ->identifier('cast.void') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index e3e1e67cda..bec217c30f 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -2,25 +2,27 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ClassReflection; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; +use function sprintf; +use function strtolower; -class ClassCaseSensitivityCheck +#[AutowiredService] +final class ClassCaseSensitivityCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkInternalClassCaseSensitivity; - - public function __construct(ReflectionProvider $reflectionProvider, bool $checkInternalClassCaseSensitivity = false) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $checkInternalClassCaseSensitivity, + ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkInternalClassCaseSensitivity = $checkInternalClassCaseSensitivity; } /** * @param ClassNameNodePair[] $pairs - * @return RuleError[] + * @return list */ public function checkClassNames(array $pairs): array { @@ -42,26 +44,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..70249cc666 --- /dev/null +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -0,0 +1,96 @@ + '_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)); + + $pos = strpos($withoutPrefixClassName, '\\'); + if ($pos === false) { + continue; + } + + $withoutPrefixClassName = substr($withoutPrefixClassName, $pos); + } + + 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..ba23578483 --- /dev/null +++ b/src/Rules/ClassNameCheck.php @@ -0,0 +1,78 @@ + + */ + 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..6a2dfdc52e --- /dev/null +++ b/src/Rules/ClassNameUsageLocation.php @@ -0,0 +1,307 @@ +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::PHPDOC_TAG_SEALED: + return sprintf('PHPDoc tag @phpstan-sealed references %s.', $part); + case self::STATIC_METHOD_CALL: + $method = $this->getMethod(); + if ($method !== null) { + return sprintf('Call to static method %s() on %s.', $method->getName(), $part); + } + + return sprintf('Call to static method on %s.', $part); + case self::PHPDOC_TAG_TEMPLATE_BOUND: + if ($this->getTemplateTagName() !== null) { + return sprintf('PHPDoc tag @template %s bound references %s.', $this->getTemplateTagName(), $part); + } + + return sprintf('PHPDoc tag @template bound references %s.', $part); + case self::PHPDOC_TAG_TEMPLATE_DEFAULT: + if ($this->getTemplateTagName() !== null) { + return sprintf('PHPDoc tag @template %s default references %s.', $this->getTemplateTagName(), $part); + } + + return sprintf('PHPDoc tag @template default references %s.', $part); + } + } + + public function createIdentifier(string $secondPart): string + { + if ($this->value === self::CLASS_IMPLEMENTS) { + return sprintf('class.implements%s', ucfirst($secondPart)); + } + if ($this->value === self::ENUM_IMPLEMENTS) { + return sprintf('enum.implements%s', ucfirst($secondPart)); + } + if ($this->value === self::INTERFACE_EXTENDS) { + return sprintf('interface.extends%s', ucfirst($secondPart)); + } + if ($this->value === self::CLASS_EXTENDS) { + return sprintf('class.extends%s', ucfirst($secondPart)); + } + if ($this->value === self::PHPDOC_TAG_TEMPLATE_BOUND) { + return sprintf('generics.%sBound', $secondPart); + } + if ($this->value === self::PHPDOC_TAG_TEMPLATE_DEFAULT) { + return sprintf('generics.%sDefault', $secondPart); + } + + return sprintf('%s.%s', $this->value, $secondPart); + } + +} diff --git a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php index fa3bf53a6d..ef86b84686 100644 --- a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -5,13 +5,16 @@ use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class AccessPrivateConstantThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class AccessPrivateConstantThroughStaticRule implements Rule { public function getNodeType(): string @@ -52,8 +55,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Unsafe access to private constant %s::%s through static::.', $constant->getDeclaringClass()->getDisplayName(), - $constantName - ))->build(), + $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..b5fc2a7b32 --- /dev/null +++ b/src/Rules/Classes/AllowedSubTypesRule.php @@ -0,0 +1,70 @@ + + */ +#[RegisteredRule(level: 0)] +final class AllowedSubTypesRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $className = $classReflection->getName(); + + $parents = array_values($classReflection->getImmediateInterfaces()); + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { + $parents[] = $parentClass; + } + + $messages = []; + + foreach ($parents as $parentReflection) { + $allowedSubTypes = $parentReflection->getAllowedSubTypes(); + if ($allowedSubTypes === null) { + continue; + } + + foreach ($allowedSubTypes as $allowedSubType) { + if (!$allowedSubType->isObject()->yes()) { + continue; + } + + if ($allowedSubType->getObjectClassNames() === [$className]) { + continue 2; + } + } + + $identifierType = strtolower($classReflection->getClassTypeDescription()); + $messages[] = RuleErrorBuilder::message(sprintf( + 'Type %s is not allowed to be a subtype of %s.', + $className, + $parentReflection->getName(), + ))->identifier(sprintf('%s.disallowedSubtype', $identifierType))->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 839a8fd112..7bce7b065c 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -2,37 +2,70 @@ namespace PHPStan\Rules\Classes; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InClassNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** - * @implements Rule + * @implements Rule */ -class ClassAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class ClassAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string { - return Node\Stmt\ClassLike::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - return $this->attributesCheck->check( + $classLikeNode = $node->getOriginalNode(); + + $errors = $this->attributesCheck->check( $scope, - $node->attrGroups, - \Attribute::TARGET_CLASS, - 'class' + $classLikeNode->attrGroups, + Attribute::TARGET_CLASS, + 'class', ); + + $classReflection = $node->getClassReflection(); + if ( + $classReflection->isReadOnly() + || $classReflection->isEnum() + || $classReflection->isInterface() + ) { + $typeName = 'readonly class'; + $identifier = 'class.allowDynamicPropertiesReadonly'; + if ($classReflection->isEnum()) { + $typeName = 'enum'; + $identifier = 'enum.allowDynamicProperties'; + } + if ($classReflection->isInterface()) { + $typeName = 'interface'; + $identifier = 'interface.allowDynamicProperties'; + } + + if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName)) + ->identifier($identifier) + ->nonIgnorable() + ->build(); + } + } + + return $errors; } } diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index ded689c6ee..3beaf3d0ea 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -2,22 +2,22 @@ namespace PHPStan\Rules\Classes; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** * @implements Rule */ -class ClassConstantAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class ClassConstantAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +30,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_CLASS_CONSTANT, - 'class constant' + Attribute::TARGET_CLASS_CONSTANT, + 'class constant', ); } diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 58d5b8e65e..a8220d5e11 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -3,13 +3,22 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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; @@ -18,32 +27,27 @@ 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 +#[RegisteredRule(level: 0)] +final class ClassConstantRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private PhpVersion $phpVersion; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - PhpVersion $phpVersion + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private ClassNameCheck $classCheck, + private PhpVersion $phpVersion, + #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] + private bool $checkNonStringableDynamicAccess, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -53,20 +57,65 @@ 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))); + } + + if ($this->checkNonStringableDynamicAccess) { + $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type) => $type->isString()->yes(), + ); + + $nameType = $nameTypeResult->getType(); + if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) { + $className = $node->class instanceof Name + ? $scope->resolveName($node->class) + : $scope->getType($node->class)->describe(VerbosityLevel::typeOnly()); + + $errors[] = RuleErrorBuilder::message(sprintf('Class constant name for %s must be a string, but %s was given.', $className, $nameType->describe(VerbosityLevel::precise()))) + ->identifier('classConstant.nameNotString') + ->build(); + } + } + } + + foreach ($constantNameScopes as $constantName => $constantScope) { + $errors = array_merge($errors, $this->processSingleClassConstFetch( + $constantScope, + $node, + (string) $constantName, // @phpstan-ignore cast.useless + )); } - $constantName = $node->name->name; + return $errors; + } + + /** + * @return list + */ + private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $node, string $constantName): array + { $class = $node->class; $messages = []; - if ($class instanceof \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(), ]; } @@ -74,7 +123,9 @@ public function processNode(Node $node, Scope $scope): array } elseif ($lowercasedClassName === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className)) + ->identifier(sprintf('outOfClass.%s', $lowercasedClassName)) + ->build(), ]; } $currentClassReflection = $scope->getClassReflection(); @@ -83,8 +134,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Access to parent::%s but %s does not extend any class.', $constantName, - $currentClassReflection->getDisplayName() - ))->build(), + $currentClassReflection->getDisplayName(), + ))->identifier('class.noParent')->build(), ]; } $classType = $scope->resolveTypeByName($class); @@ -96,20 +147,51 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($constantName) === 'class') { return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $className))->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Class %s not found.', $className)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; } return [ RuleErrorBuilder::message( - sprintf('Access to constant %s on an unknown class %s.', $constantName, $className) - )->discoveringSymbolsTip()->build(), + 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)]); } $classType = $scope->resolveTypeByName($class); + if (strtolower($constantName) !== 'class') { + foreach ($classType->getObjectClassReflections() as $classTypeReflection) { + if (!$classTypeReflection->isTrait()) { + continue; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot access constant %s on trait %s.', + $constantName, + $classTypeReflection->getDisplayName(), + ))->identifier('classConstant.onTrait')->build(), + ]; + } + } + + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($className); + if ($locationClassReflection->hasConstant($constantName)) { + $locationData['classConstant'] = $locationClassReflection->getConstant($constantName); + } + + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($className, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_CONSTANT_ACCESS, $locationData), + ); } if (strtolower($constantName) === 'class') { @@ -118,11 +200,9 @@ public function processNode(Node $node, Scope $scope): array } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $class, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class), sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)), - static function (Type $type) use ($constantName): bool { - return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(); - } + static fn (Type $type): bool => $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -133,14 +213,16 @@ static function (Type $type) use ($constantName): bool { if (!$this->phpVersion->supportsClassConstantOnExpression()) { return [ RuleErrorBuilder::message('Accessing ::class constant on an expression is supported only on PHP 8.0 and later.') + ->identifier('classConstant.notSupported') ->nonIgnorable() ->build(), ]; } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if (!$class instanceof Node\Scalar\String_ && $classType->isString()->yes()) { return [ RuleErrorBuilder::message('Accessing ::class constant on a dynamic string is not supported in PHP.') + ->identifier('classConstant.dynamicString') ->nonIgnorable() ->build(), ]; @@ -148,7 +230,7 @@ static function (Type $type) use ($constantName): bool { } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return $messages; } @@ -163,12 +245,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; } @@ -177,8 +259,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(), ]); } @@ -189,7 +271,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/ConsistentConstructorHelper.php b/src/Rules/Classes/ConsistentConstructorHelper.php new file mode 100644 index 0000000000..d266cc0b36 --- /dev/null +++ b/src/Rules/Classes/ConsistentConstructorHelper.php @@ -0,0 +1,32 @@ +hasConsistentConstructor()) { + if ($classReflection->hasConstructor()) { + return $classReflection->getConstructor(); + } + + return new DummyConstructorReflection($classReflection); + } + + $parent = $classReflection->getParentClass(); + if ($parent === null) { + return null; + } + + return $this->findConsistentConstructor($parent); + } + +} diff --git a/src/Rules/Classes/DuplicateClassDeclarationRule.php b/src/Rules/Classes/DuplicateClassDeclarationRule.php new file mode 100644 index 0000000000..bf8ac7f05d --- /dev/null +++ b/src/Rules/Classes/DuplicateClassDeclarationRule.php @@ -0,0 +1,66 @@ + + */ +final class DuplicateClassDeclarationRule implements Rule +{ + + public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $thisClass = $node->getClassReflection(); + $className = $thisClass->getName(); + $allClasses = $this->reflector->reflectAllClasses(); + $filteredClasses = []; + foreach ($allClasses as $reflectionClass) { + if ($reflectionClass->getName() !== $className) { + continue; + } + + $filteredClasses[] = $reflectionClass; + } + + if (count($filteredClasses) < 2) { + return []; + } + + $filteredClasses = array_filter($filteredClasses, static fn (ReflectionClass $class) => $class->getStartLine() !== $thisClass->getNativeReflection()->getStartLine()); + + $identifierType = strtolower($thisClass->getClassTypeDescription()); + + return [ + RuleErrorBuilder::message(sprintf( + "Class %s declared multiple times:\n%s", + $thisClass->getDisplayName(), + implode("\n", array_map(fn (ReflectionClass $class) => sprintf('- %s:%d', $this->relativePathHelper->getRelativePath($class->getFileName() ?? 'unknown'), $class->getStartLine()), $filteredClasses)), + ))->identifier(sprintf('%s.duplicate', $identifierType))->build(), + ]; + } + +} diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index eaee0ead9f..6fe86ed9bc 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -3,17 +3,24 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PhpParser\Node\Stmt\ClassConst; +use PhpParser\Node\Stmt\EnumCase; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use function array_key_exists; +use function is_string; use function sprintf; use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassNode> + * @implements Rule */ -class DuplicateDeclarationRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +final class DuplicateDeclarationRule implements Rule { public function getNodeType(): string @@ -23,24 +30,41 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $scope->getClassReflection(); - if ($classReflection === null) { - throw new \PHPStan\ShouldNotHappenException(); - } + $classReflection = $node->getClassReflection(); + + $identifierType = strtolower($classReflection->getClassTypeDescription()); $errors = []; - $declaredClassConstants = []; - foreach ($node->getOriginalNode()->getConstants() as $constDecl) { - foreach ($constDecl->consts as $const) { - if (array_key_exists($const->name->name, $declaredClassConstants)) { + $declaredClassConstantsOrEnumCases = []; + foreach ($node->getOriginalNode()->stmts as $stmtNode) { + if ($stmtNode instanceof EnumCase) { + if (array_key_exists($stmtNode->name->name, $declaredClassConstantsOrEnumCases)) { $errors[] = RuleErrorBuilder::message(sprintf( - 'Cannot redeclare constant %s::%s.', + 'Cannot redeclare enum case %s::%s.', $classReflection->getDisplayName(), - $const->name->name - ))->line($const->getLine())->nonIgnorable()->build(); + $stmtNode->name->name, + ))->identifier(sprintf('%s.duplicateEnumCase', $identifierType)) + ->line($stmtNode->getStartLine()) + ->nonIgnorable() + ->build(); } else { - $declaredClassConstants[$const->name->name] = true; + $declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true; + } + } elseif ($stmtNode instanceof ClassConst) { + foreach ($stmtNode->consts as $classConstNode) { + if (array_key_exists($classConstNode->name->name, $declaredClassConstantsOrEnumCases)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare constant %s::%s.', + $classReflection->getDisplayName(), + $classConstNode->name->name, + ))->identifier(sprintf('%s.duplicateConstant', $identifierType)) + ->line($classConstNode->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true; + } } } } @@ -52,8 +76,11 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), - $property->name->name - ))->line($property->getLine())->nonIgnorable()->build(); + $property->name->name, + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($property->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$property->name->name] = true; } @@ -69,7 +96,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $propertyName = $param->var->name; @@ -78,8 +105,11 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), - $propertyName - ))->line($param->getLine())->nonIgnorable()->build(); + $propertyName, + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($param->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$propertyName] = true; } @@ -89,8 +119,11 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare method %s::%s().', $classReflection->getDisplayName(), - $method->name->name - ))->line($method->getStartLine())->nonIgnorable()->build(); + $method->name->name, + ))->identifier(sprintf('%s.duplicateMethod', $identifierType)) + ->line($method->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredFunctions[strtolower($method->name->name)] = true; } diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php new file mode 100644 index 0000000000..fc5d56ba8a --- /dev/null +++ b/src/Rules/Classes/EnumSanityRule.php @@ -0,0 +1,228 @@ + + */ +#[RegisteredRule(level: 0)] +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 9c39351411..4dd7daf602 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -4,28 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -39,18 +41,33 @@ public function processNode(Node $node, Scope $scope): array return []; } $extendedClassName = (string) $node->extends; - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; } + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($extendedClassName, $node->extends)], + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_EXTENDS, [ + 'currentClassName' => $currentClassName, + ]), + ); + if (!$this->reflectionProvider->hasClass($extendedClassName)) { if (!$scope->isInClassExists($extendedClassName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s extends unknown class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + $extendedClassName, + )) + ->identifier('class.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedClassName); @@ -58,26 +75,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', - $extendedClassName - ))->build(); + $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 c0b83b1e4b..1ad6e48671 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -5,32 +5,36 @@ use PhpParser\Node; use PhpParser\Node\Expr\Instanceof_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -41,7 +45,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 +59,59 @@ public function processNode(Node $node, Scope $scope): array ], true)) { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName))->line($class->getLine())->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName)) + ->identifier(sprintf('outOfClass.%s', $lowercaseName)) + ->line($class->getStartLine()) + ->build(), ]; } return []; } + $errors = []; + if (!$this->reflectionProvider->hasClass($name)) { if ($scope->isInClassExists($name)) { return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Class %s not found.', $name)) + ->identifier('class.notFound') + ->line($class->getStartLine()); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $name))->line($class->getLine())->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } elseif ($this->checkClassCaseSensitivity) { - return $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $class)]); } - return []; + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($name, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANCEOF), + $this->checkClassCaseSensitivity, + ), + ); + + $classReflection = $this->reflectionProvider->getClass($name); + + if ($classReflection->isTrait()) { + $expressionType = $scope->getType($node->expr); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and trait %s will always evaluate to false.', + $expressionType->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier('instanceof.trait')->build(); + } + + return $errors; } } diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index a26b0b6387..a624081a8f 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -4,52 +4,62 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + 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 +75,32 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->traits as $trait) { $traitName = (string) $trait; if (!$this->reflectionProvider->hasClass($traitName)) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName))->nonIgnorable()->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName)) + ->identifier('trait.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } else { $reflection = $this->reflectionProvider->getClass($traitName); if ($reflection->isClass()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $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 5e3bef9fb3..8d270048ed 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -4,28 +4,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,26 +38,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()->discoveringSymbolsTip()->build(); + $implementedClassName, + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -62,14 +75,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..05d830cd16 --- /dev/null +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -0,0 +1,104 @@ + + */ +#[RegisteredRule(level: 0)] +final class ExistingClassesInEnumImplementsRule implements Rule +{ + + public function __construct( + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + 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 0dfc51bf64..345c3663c1 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -4,28 +4,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,22 +38,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()->discoveringSymbolsTip()->build(); + $extendedInterfaceName, + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedInterfaceName); @@ -58,14 +71,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..7c866cc30e 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,31 +4,39 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ErrorType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; 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 +#[RegisteredRule(level: 4)] +final class ImpossibleInstanceOfRule implements Rule { - private bool $checkAlwaysTrueInstanceof; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - bool $checkAlwaysTrueInstanceof, - bool $treatPhpDocTypesAsCertain + private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->checkAlwaysTrueInstanceof = $checkAlwaysTrueInstanceof; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -38,29 +46,33 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $instanceofType = $scope->getType($node); - $expressionType = $scope->getType($node->expr); - 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()) { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', + static fn (Type $type): bool => !$allowed->isSuperTypeOf($type)->yes(), + ); + if (!$typeResult->getType() instanceof ErrorType && !$allowed->isSuperTypeOf($typeResult->getType())->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(), ]; } } + $instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$instanceofType instanceof ConstantBooleanType) { return []; } @@ -70,33 +82,48 @@ public function processNode(Node $node, Scope $scope): array 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..a263c1d49b --- /dev/null +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -0,0 +1,34 @@ + + */ +#[RegisteredRule(level: 0)] +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 94237c0b6d..6f8b322975 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -5,40 +5,49 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\RegisteredRule; 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\RestrictedUsage\RestrictedMethodUsageExtension; +use PHPStan\Rules\RestrictedUsage\RewrittenDeclaringClassMethodReflection; +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 +#[RegisteredRule(level: 0)] +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 Container $container, + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + private ClassNameCheck $classCheck, + private ConsistentConstructorHelper $consistentConstructorHelper, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -56,10 +65,8 @@ public function processNode(Node $node, Scope $scope): array } /** - * @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, bool $isName, Node $node, Scope $scope): array { @@ -69,7 +76,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ if ($lowercasedClass === 'static') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.static') + ->build(), ]; } @@ -86,6 +95,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ && $constructor instanceof PhpMethodReflection && !$constructor->isFinal()->yes() && !$constructor->getPrototype()->isAbstract() + && $this->consistentConstructorHelper->findConsistentConstructor($classReflection) === null ) { return []; } @@ -93,14 +103,18 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } elseif ($lowercasedClass === 'self') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.self') + ->build(), ]; } $classReflection = $scope->getClassReflection(); } elseif ($lowercasedClass === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.parent') + ->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -109,8 +123,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ '%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(); @@ -120,31 +134,46 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $node->class), - ]); } + $messages = $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node->class), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANTIATION)); + $classReflection = $this->reflectionProvider->getClass($class); } + if ($classReflection->isEnum() && $isName) { + return [ + RuleErrorBuilder::message( + sprintf('Cannot instantiate enum %s.', $classReflection->getDisplayName()), + )->identifier('new.enum')->build(), + ]; + } + if (!$isStatic && $classReflection->isInterface() && $isName) { return [ RuleErrorBuilder::message( - sprintf('Cannot instantiate interface %s.', $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()) - )->build(), + sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), + )->identifier('new.abstract')->build(), ]; } @@ -157,8 +186,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 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(), ]); } @@ -172,8 +201,32 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $classReflection->getDisplayName(), $constructorReflection->isPrivate() ? 'private' : 'protected', $constructorReflection->getDeclaringClass()->getDisplayName(), - $constructorReflection->getName() - ))->build(); + $constructorReflection->getName(), + )) + ->identifier(sprintf('new.%sConstructor', $constructorReflection->isPrivate() ? 'private' : 'protected')) + ->build(); + } + + /** @var RestrictedMethodUsageExtension[] $restrictedUsageExtensions */ + $restrictedUsageExtensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + + foreach ($restrictedUsageExtensions as $extension) { + $restrictedUsage = $extension->isRestrictedMethodUsage($constructorReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + if ($classReflection->getName() !== $constructorReflection->getDeclaringClass()->getName()) { + $rewrittenConstructorReflection = new RewrittenDeclaringClassMethodReflection($classReflection, $constructorReflection); + $rewrittenRestrictedUsage = $extension->isRestrictedMethodUsage($rewrittenConstructorReflection, $scope); + if ($rewrittenRestrictedUsage === null) { + continue; + } + } + + $messages[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); } $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); @@ -182,64 +235,79 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), - $constructorReflection->getVariants() + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), $node, - [ - 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.', - 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.', - 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.', - 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.', - 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.', - 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.', - '', // constructor does not have a return type - 'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName, - 'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.', - 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', - 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', - ] + 'new', + $constructorReflection->acceptsNamedArguments(), + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.', + '%s of class ' . $classDisplayName . ' constructor expects %s, %s given.', + '', // constructor does not have a return type + ' %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName, + 'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', + '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', + 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } /** - * @param \PhpParser\Node\Expr\New_ $node $node - * @param Scope $scope + * @param Node\Expr\New_ $node * @return array */ private function getClassNames(Node $node, Scope $scope): array { - if ($node->class instanceof \PhpParser\Node\Name) { + if ($node->class instanceof Node\Name) { return [[(string) $node->class, true]]; } if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassType = $scope->getType($node); - if (!$anonymousClassType instanceof TypeWithClassName) { - throw new \PHPStan\ShouldNotHappenException(); + $classNames = $scope->getType($node)->getObjectClassNames(); + if ($classNames === []) { + throw new ShouldNotHappenException(); } - return [[$anonymousClassType->getClassName(), true]]; + return array_map( + static fn (string $className) => [$className, true], + $classNames, + ); } $type = $scope->getType($node->class); + if ($type->isClassString()->yes()) { + $concretes = array_filter( + $type->getClassStringObjectType()->getObjectClassReflections(), + static fn (ClassReflection $classReflection): bool => !$classReflection->isAbstract() && !$classReflection->isInterface(), + ); + + if (count($concretes) > 0) { + return array_map( + static fn (ClassReflection $classReflection): array => [$classReflection->getName(), true], + $concretes, + ); + } + } + return array_merge( array_map( - static function (ConstantStringType $type): array { - return [$type->getValue(), true]; - }, - TypeUtils::getConstantStrings($type) + static fn (ConstantStringType $type): array => [$type->getValue(), true], + $type->getConstantStrings(), ), array_map( - static function (string $name): array { - return [$name, false]; - }, - TypeUtils::getDirectClassNames($type) - ) + static fn (string $name): array => [$name, false], + $type->getObjectClassNames(), + ), ); } diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index 0e57df511f..ef2e2c167a 100644 --- a/src/Rules/Classes/InvalidPromotedPropertiesRule.php +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.php @@ -4,42 +4,41 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function is_string; +use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidPromotedPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidPromotedPropertiesRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string { - return Node::class; + return Node\FunctionLike::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Expr\ArrowFunction - && !$node instanceof Node\Stmt\ClassMethod - && !$node instanceof Node\Expr\Closure - && !$node instanceof Node\Stmt\Function_ - ) { - return []; - } - $hasPromotedProperties = false; - foreach ($node->params as $param) { - if ($param->flags === 0) { + + foreach ($node->getParams() as $param) { + if ($param->flags !== 0) { + $hasPromotedProperties = true; + break; + } + + if ($param->hooks === []) { continue; } @@ -54,38 +53,40 @@ public function processNode(Node $node, Scope $scope): array if (!$this->phpVersion->supportsPromotedProperties()) { return [ RuleErrorBuilder::message( - 'Promoted properties are supported only on PHP 8.0 and later.' - )->nonIgnorable()->build(), + '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->name->toLowerString() !== '__construct' + && $node->getAttribute('originalTraitMethodName') !== '__construct') ) { return [ RuleErrorBuilder::message( - 'Promoted properties can be in constructor only.' - )->nonIgnorable()->build(), + 'Promoted properties can be in constructor only.', + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } - if ($node->stmts === null) { + if ($node->getStmts() === null) { return [ RuleErrorBuilder::message( - 'Promoted properties are not allowed in abstract constructors.' - )->nonIgnorable()->build(), + 'Promoted properties are not allowed in abstract constructors.', + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } $errors = []; - foreach ($node->params as $param) { + foreach ($node->getParams() as $param) { if ($param->flags === 0) { continue; } if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$param->variadic) { @@ -94,9 +95,8 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $param->var->name; $errors[] = RuleErrorBuilder::message( - sprintf('Promoted property parameter $%s can not be variadic.', $propertyName) - )->nonIgnorable()->line($param->getLine())->build(); - continue; + 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..a849ecac8c --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -0,0 +1,376 @@ + $globalTypeAliases + */ + public function __construct( + #[AutowiredParameter(ref: '%typeAliases%')] + private array $globalTypeAliases, + private ReflectionProvider $reflectionProvider, + private TypeNodeResolver $typeNodeResolver, + private MissingTypehintCheck $missingTypehintCheck, + private ClassNameCheck $classCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericObjectTypeCheck $genericObjectTypeCheck, + #[AutowiredParameter] + private bool $checkMissingTypehints, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, + ) + { + } + + /** + * @return list + */ + public function check(Scope $scope, ClassReflection $reflection, ClassLike $node): array + { + $errors = []; + foreach ($this->checkInTraitDefinitionContext($reflection) as $error) { + $errors[] = $error; + } + foreach ($this->checkInTraitUseContext($scope, $reflection, $reflection, $node) as $error) { + $errors[] = $error; + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $reflection): array + { + $phpDoc = $reflection->getResolvedPhpDoc(); + if ($phpDoc === null) { + return []; + } + + $nameScope = $phpDoc->getNullableNameScope(); + $resolveName = static function (string $name) use ($nameScope): string { + if ($nameScope === null) { + return $name; + } + + return $nameScope->resolveStringName($name); + }; + + $errors = []; + $className = $reflection->getDisplayName(); + + $importedAliases = []; + + foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { + $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName)) + ->identifier('class.notFound') + ->build(); + continue; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + $typeAliases = $importedFromReflection->getTypeAliases(); + + if (!array_key_exists($importedAlias, $typeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName)) + ->identifier('typeAlias.notFound') + ->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className)) + ->identifier('typeAlias.duplicate') + ->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $importedAs = $typeAliasImportTag->getImportedAs(); + if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->identifier('typeAlias.invalidName')->build(); + continue; + } + + $importedAliases[] = $aliasName; + } + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + + if (in_array($aliasName, $importedAliases, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolvedName)) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (!$this->isAliasNameValid($aliasName, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName)) + ->identifier('typeAlias.invalidName') + ->build(); + continue; + } + + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + if ($this->hasErrorType($resolvedType, $aliasName, $errors)) { + continue; + } + + if (!$this->checkMissingTypehints) { + continue; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($resolvedType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with no value type specified in iterable type %s.', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($resolvedType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with generic %s but does not specify its types: %s', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($resolvedType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with no signature specified for %s.', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitUseContext( + Scope $scope, + ClassReflection $reflection, + ClassReflection $implementingClassReflection, + ClassLike $node, + ): array + { + if ($reflection->getNativeReflection()->getName() === $implementingClassReflection->getName()) { + $phpDoc = $reflection->getResolvedPhpDoc(); + } else { + $phpDoc = $reflection->getTraitContextResolvedPhpDoc($implementingClassReflection); + } + if ($phpDoc === null) { + return []; + } + + $errors = []; + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + $throwawayErrors = []; + if ($this->hasErrorType($resolvedType, $aliasName, $throwawayErrors)) { + continue; + } + foreach ($resolvedType->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errorBuilder = RuleErrorBuilder::message(sprintf('Type alias %s contains unknown class %s.', $aliasName, $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s contains invalid type %s.', $aliasName, $class)) + ->identifier('typeAlias.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::TYPE_ALIAS, [ + 'typeAliasName' => $aliasName, + ]), $this->checkClassCaseSensitivity), + ); + } + } + + if ($this->unresolvableTypeHelper->containsUnresolvableType($resolvedType)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s contains unresolvable type.', $aliasName)) + ->identifier('typeAlias.unresolvableType') + ->build(); + } + + $escapedTypeAlias = SprintfHelper::escapeFormatString($aliasName); + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $resolvedType, + sprintf( + 'Type alias %s contains generic type %%s but %%s %%s is not generic.', + $escapedTypeAlias, + ), + sprintf( + 'Generic type %%s in type alias %s does not specify all template types of %%s %%s: %%s', + $escapedTypeAlias, + ), + sprintf( + 'Generic type %%s in type alias %s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTypeAlias, + ), + sprintf( + 'Type %%s in generic type %%s in type alias %s is not subtype of template type %%s of %%s %%s.', + $escapedTypeAlias, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in type alias %s is in conflict with %%s template type %%s of %%s %%s.', + $escapedTypeAlias, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in type alias %s is redundant, template type %%s of %%s %%s has the same variance.', + $escapedTypeAlias, + ), + )); + } + + return $errors; + } + + private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool + { + if ($nameScope === null) { + return true; + } + + $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); + return ($aliasNameResolvedType->isObject()->yes() && !in_array($aliasName, ['self', 'parent'], true)) + || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + } + + /** + * @param list $errors + * @param-out list $errors + */ + private function hasErrorType(Type $type, string $aliasName, array &$errors): bool + { + $foundError = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { + if ($foundError) { + return $type; + } + + if ($type instanceof CircularTypeAliasErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.circular') + ->build(); + $foundError = true; + return $type; + } + + if ($type instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.invalidType') + ->build(); + $foundError = true; + return $type; + } + + return $traverse($type); + }); + + return $foundError; + } + +} diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index 51c69d37c8..597d4c04c1 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -3,45 +3,20 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; -use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; -use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\CircularTypeAliasErrorType; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeTraverser; /** * @implements Rule */ -class LocalTypeAliasesRule implements Rule +#[RegisteredRule(level: 0)] +final class LocalTypeAliasesRule implements Rule { - /** @var array */ - private array $globalTypeAliases; - - private ReflectionProvider $reflectionProvider; - - private TypeNodeResolver $typeNodeResolver; - - /** - * @param array $globalTypeAliases - */ - public function __construct( - array $globalTypeAliases, - ReflectionProvider $reflectionProvider, - TypeNodeResolver $typeNodeResolver - ) + public function __construct(private LocalTypeAliasesCheck $check) { - $this->globalTypeAliases = $globalTypeAliases; - $this->reflectionProvider = $reflectionProvider; - $this->typeNodeResolver = $typeNodeResolver; } public function getNodeType(): string @@ -51,121 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $reflection = $node->getClassReflection(); - $phpDoc = $reflection->getResolvedPhpDoc(); - if ($phpDoc === null) { - return []; - } - - $nameScope = $phpDoc->getNullableNameScope(); - $resolveName = static function (string $name) use ($nameScope): string { - if ($nameScope === null) { - return $name; - } - - return $nameScope->resolveStringName($name); - }; - - $errors = []; - $className = $reflection->getName(); - - $importedAliases = []; - - foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { - $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); - $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFromClassName = $typeAliasImportTag->getImportedFrom(); - - if (!$this->reflectionProvider->hasClass($importedFromClassName)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); - $typeAliases = $importedFromReflection->getTypeAliases(); - - if (!array_key_exists($importedAlias, $typeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - $importedAs = $typeAliasImportTag->getImportedAs(); - if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build(); - continue; - } - - $importedAliases[] = $aliasName; - } - - foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { - $aliasName = $typeAliasTag->getAliasName(); - - if (in_array($aliasName, $importedAliases, true)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build(); - continue; - } - - if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - if (!$this->isAliasNameValid($aliasName, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName))->build(); - continue; - } - - $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); - $foundError = false; - TypeTraverser::map($resolvedType, static function (\PHPStan\Type\Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): \PHPStan\Type\Type { - if ($foundError) { - return $type; - } - - if ($type instanceof CircularTypeAliasErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - if ($type instanceof ErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - return $traverse($type); - }); - } - - return $errors; - } - - private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool - { - if ($nameScope === null) { - return true; - } - - $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); - return ($aliasNameResolvedType instanceof ObjectType && !in_array($aliasName, ['self', 'parent'], true)) - || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/src/Rules/Classes/LocalTypeTraitAliasesRule.php b/src/Rules/Classes/LocalTypeTraitAliasesRule.php new file mode 100644 index 0000000000..0b8533975a --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitAliasesRule.php @@ -0,0 +1,41 @@ + + */ +#[RegisteredRule(level: 0)] +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..da4040f6f6 --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php @@ -0,0 +1,37 @@ + + */ +#[RegisteredRule(level: 0)] +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..88e5e3a450 --- /dev/null +++ b/src/Rules/Classes/MethodTagCheck.php @@ -0,0 +1,281 @@ + + */ + 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..311efba203 --- /dev/null +++ b/src/Rules/Classes/MethodTagRule.php @@ -0,0 +1,36 @@ + + */ +#[RegisteredRule(level: 2)] +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..3b48efc73d --- /dev/null +++ b/src/Rules/Classes/MethodTagTraitRule.php @@ -0,0 +1,41 @@ + + */ +#[RegisteredRule(level: 2)] +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..6a1210474d --- /dev/null +++ b/src/Rules/Classes/MethodTagTraitUseRule.php @@ -0,0 +1,37 @@ + + */ +#[RegisteredRule(level: 2)] +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..ecdfff0d92 --- /dev/null +++ b/src/Rules/Classes/MixinCheck.php @@ -0,0 +1,184 @@ + + */ + 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 1a4c6797c4..330b403e24 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -4,129 +4,29 @@ 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\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\VerbosityLevel; /** - * @implements Rule + * @implements Rule */ -class MixinRule implements Rule +#[RegisteredRule(level: 2)] +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 UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkClassCaseSensitivity; - - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkClassCaseSensitivity - ) + public function __construct(private MixinCheck $check) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - // anonymous class - return []; - } - - $className = (string) $node->namespacedName; - $docComment = $node->getDocComment(); - if ($docComment === null) { - 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 ( - $this->unresolvableTypeHelper->containsUnresolvableType($type) - ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.')->build(); - continue; - } - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $type, - 'PHPDoc tag @mixin contains generic type %s but 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))->discoveringSymbolsTip()->build(); - } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class))->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $node), - ]) - ); - } - } - } - - return $errors; + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/src/Rules/Classes/MixinTraitRule.php b/src/Rules/Classes/MixinTraitRule.php new file mode 100644 index 0000000000..66135451a9 --- /dev/null +++ b/src/Rules/Classes/MixinTraitRule.php @@ -0,0 +1,43 @@ + + */ +#[RegisteredRule(level: 2)] +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..7e9c0c92fb --- /dev/null +++ b/src/Rules/Classes/MixinTraitUseRule.php @@ -0,0 +1,37 @@ + + */ +#[RegisteredRule(level: 2)] +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/NewStaticInAbstractClassStaticMethodRule.php b/src/Rules/Classes/NewStaticInAbstractClassStaticMethodRule.php new file mode 100644 index 0000000000..0b2842648b --- /dev/null +++ b/src/Rules/Classes/NewStaticInAbstractClassStaticMethodRule.php @@ -0,0 +1,64 @@ + + */ +final class NewStaticInAbstractClassStaticMethodRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\New_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + if (strtolower($node->class->toString()) !== 'static') { + return []; + } + + $classReflection = $scope->getClassReflection(); + if (!$classReflection->isAbstract()) { + return []; + } + + $inMethod = $scope->getFunction(); + if (!$inMethod instanceof PhpMethodFromParserNodeReflection) { + return []; + } + + if (!$inMethod->isStatic()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe usage of new static() in abstract class %s in static method %s().', + $classReflection->getDisplayName(), + $inMethod->getName(), + )) + ->identifier('new.staticInAbstractClassStaticMethod') + ->tip(sprintf('Direct call to %s::%s() would crash because an abstract class cannot be instantiated.', $classReflection->getName(), $inMethod->getName())) + ->build(), + ]; + } + +} diff --git a/src/Rules/Classes/NewStaticRule.php b/src/Rules/Classes/NewStaticRule.php index e93c93f68c..d103409393 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -4,16 +4,28 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class NewStaticRule implements Rule +#[RegisteredRule(level: 0)] +final class NewStaticRule implements Rule { + public function __construct( + private PhpVersion $phpVersion, + private ConsistentConstructorHelper $consistentConstructorHelper, + ) + { + } + public function getNodeType(): string { return Node\Expr\New_::class; @@ -40,9 +52,14 @@ public function processNode(Node $node, Scope $scope): array $messages = [ RuleErrorBuilder::message('Unsafe usage of new static().') + ->identifier('new.static') ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static') ->build(), ]; + $consistentConstructor = $this->consistentConstructorHelper->findConsistentConstructor($classReflection); + if ($consistentConstructor !== null) { + return []; + } if (!$classReflection->hasConstructor()) { return $messages; } @@ -52,17 +69,36 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($constructor instanceof PhpMethodReflection) { - if ($constructor->isFinal()->yes()) { + foreach ($classReflection->getImmediateInterfaces() as $interface) { + if ($interface->hasConstructor()) { return []; } + } + if ($constructor->isFinal()->yes()) { + return []; + } + + if ($constructor instanceof PhpMethodReflection) { $prototype = $constructor->getPrototype(); if ($prototype->isAbstract()) { return []; } } + if ( + $this->phpVersion->supportsAbstractTraitMethods() + && $scope->isInTrait() + ) { + $traitReflection = $scope->getTraitReflection(); + if ($traitReflection->hasConstructor()) { + $isAbstract = $traitReflection->getConstructor()->isAbstract(); + if ($isAbstract === true || ($isAbstract instanceof TrinaryLogic && $isAbstract->yes())) { + return []; + } + } + } + return $messages; } diff --git a/src/Rules/Classes/NonClassAttributeClassRule.php b/src/Rules/Classes/NonClassAttributeClassRule.php index 3a8bcedbbf..7e11b6feed 100644 --- a/src/Rules/Classes/NonClassAttributeClassRule.php +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -4,15 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; +use function strtolower; /** * @implements Rule */ -class NonClassAttributeClassRule implements Rule +#[RegisteredRule(level: 0)] +final class NonClassAttributeClassRule implements Rule { public function getNodeType(): string @@ -36,23 +41,29 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope - * @return RuleError[] + * @return list */ private function check(Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); if (!$classReflection->isClass()) { return [ - RuleErrorBuilder::message('Interface cannot be an Attribute class.')->build(), + RuleErrorBuilder::message(sprintf( + '%s cannot be an Attribute class.', + $classReflection->getClassTypeDescription(), + )) + ->identifier(sprintf('attribute.%s', strtolower($classReflection->getClassTypeDescription()))) + ->build(), ]; } if ($classReflection->isAbstract()) { return [ - RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName())) + ->identifier('attribute.abstract') + ->build(), ]; } @@ -62,7 +73,9 @@ private function check(Scope $scope): array if (!$classReflection->getConstructor()->isPublic()) { return [ - RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName())) + ->identifier('attribute.constructorNotPublic') + ->build(), ]; } diff --git a/src/Rules/Classes/PropertyTagCheck.php b/src/Rules/Classes/PropertyTagCheck.php new file mode 100644 index 0000000000..6b4a42c905 --- /dev/null +++ b/src/Rules/Classes/PropertyTagCheck.php @@ -0,0 +1,262 @@ + + */ + 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..808c4a44ac --- /dev/null +++ b/src/Rules/Classes/PropertyTagRule.php @@ -0,0 +1,32 @@ + + */ +#[RegisteredRule(level: 2)] +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..7af323dfcf --- /dev/null +++ b/src/Rules/Classes/PropertyTagTraitRule.php @@ -0,0 +1,41 @@ + + */ +#[RegisteredRule(level: 2)] +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..404a5460d4 --- /dev/null +++ b/src/Rules/Classes/PropertyTagTraitUseRule.php @@ -0,0 +1,37 @@ + + */ +#[RegisteredRule(level: 2)] +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..c29315a4ff --- /dev/null +++ b/src/Rules/Classes/ReadOnlyClassRule.php @@ -0,0 +1,60 @@ + + */ +#[RegisteredRule(level: 0)] +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..29dd6dce68 --- /dev/null +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -0,0 +1,88 @@ + + */ +#[RegisteredRule(level: 2)] +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..4e0f368308 --- /dev/null +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -0,0 +1,60 @@ + + */ +#[RegisteredRule(level: 2)] +final class RequireImplementsRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + $errors = []; + foreach ($classReflection->getTraits(true) as $trait) { + $implementsTags = $trait->getRequireImplementsTags(); + foreach ($implementsTags as $implementsTag) { + $type = $implementsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->implementsInterface($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to implement %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingImplements') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/TraitAttributeClassRule.php b/src/Rules/Classes/TraitAttributeClassRule.php index 454d063a3a..3fb6e3091d 100644 --- a/src/Rules/Classes/TraitAttributeClassRule.php +++ b/src/Rules/Classes/TraitAttributeClassRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule */ -class TraitAttributeClassRule implements Rule +#[RegisteredRule(level: 0)] +final class TraitAttributeClassRule implements Rule { public function getNodeType(): string @@ -25,7 +27,9 @@ public function processNode(Node $node, Scope $scope): array $name = $attr->name->toLowerString(); if ($name === 'attribute') { return [ - RuleErrorBuilder::message('Trait cannot be an Attribute class.')->build(), + RuleErrorBuilder::message('Trait cannot be an Attribute class.') + ->identifier('attribute.trait') + ->build(), ]; } } diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index fcfbfd1547..ca734d1867 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -6,22 +6,27 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\ShouldNotHappenException; +use function array_filter; +use function array_map; +use function array_values; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class UnusedConstructorParametersRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 1)] +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 @@ -31,46 +36,44 @@ public function getNodeType(): string 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 []; } - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { + if (count($originalNode->params) === 0) { return []; } - - $originalNode = $node->getOriginalNode(); - if (strtolower($method->getName()) !== '__construct' || $originalNode->stmts === null) { + if ($node->getClassReflection()->isAttributeClass()) { return []; } - if (count($originalNode->params) === 0) { - return []; + foreach ($node->getClassReflection()->getInterfaces() as $interface) { + if ($interface->hasConstructor()) { + return []; + } } $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()) + SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()), ); - if ($scope->getClassReflection()->isAnonymous()) { + if ($node->getClassReflection()->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; } return $this->check->getUnusedParameters( $scope, - array_map(static function (Param $parameter): string { - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { + throw new ShouldNotHappenException(); } - return $parameter->var->name; - }, array_values(array_filter($originalNode->params, static function (Param $parameter): bool { - return $parameter->flags === 0; - }))), + return $parameter->var; + }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), $originalNode->stmts, $message, 'constructor.unusedParameter', - [] ); } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index f4dc1fde2d..d9945748b3 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -2,27 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 + * @implements Rule */ -class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 4)] +final class BooleanAndConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -31,16 +39,17 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $errors = []; $originalNode = $node->getOriginalNode(); + $nodeText = $originalNode->getOperatorSigil(); $leftType = $this->helper->getBooleanType($scope, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $originalNode): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -49,62 +58,104 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of && is always %s.', - $leftType->getValue() ? 'true' : 'false' - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); $rightType = $this->helper->getBooleanType( $rightScope, - $originalNode->right + $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), - $originalNode->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($originalNode->right->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } } - if (count($errors) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of && is always %s.', - $nodeType->getValue() ? 'true' : 'false' - )))->build(); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, + $nodeType->getValue() ? 'true' : 'false', + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $errors[] = $errorBuilder->build(); + } } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index f87f3d342b..3a462cc836 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -2,36 +2,43 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class BooleanNotConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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 +52,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 cb521e5035..199410f197 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -2,27 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 + * @implements Rule */ -class BooleanOrConstantConditionRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 4)] +final class BooleanOrConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -31,16 +39,17 @@ public function getNodeType(): string } 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, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -49,61 +58,104 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of || is always %s.', - $leftType->getValue() ? 'true' : 'false' - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); $rightType = $this->helper->getBooleanType( $rightScope, - $originalNode->right + $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), - $originalNode->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($originalNode->right->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); + } } - if (count($messages) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of || is always %s.', - $nodeType->getValue() ? 'true' : 'false' - )))->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, + $nodeType->getValue() ? 'true' : 'false', + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $messages[] = $errorBuilder->build(); + } } } diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 18de49a6c7..7fc6692103 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -6,35 +6,31 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; -class ConstantConditionRuleHelper +#[AutowiredService] +final class ConstantConditionRuleHelper { - private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + #[AutowiredParameter] + 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 +40,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..cc9a2ebdd7 --- /dev/null +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -0,0 +1,96 @@ + + */ +#[RegisteredRule(level: 4)] +final class ConstantLooseComparisonRule implements Rule +{ + + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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 index 2b24587113..5c5e193222 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -3,32 +3,33 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** * @implements Rule */ -class DoWhileLoopConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class DoWhileLoopConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -52,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -75,15 +76,21 @@ public function processNode(Node $node, Scope $scope): array if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Do-while loop condition is always %s.', - $exprType->getValue() ? 'true' : 'false' - )))->line($node->getCond()->getLine())->build(), + $exprType->getValue() ? 'true' : 'false', + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(), ]; } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 734ccabf49..bcf9ba5dde 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -2,36 +2,43 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class ElseIfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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,22 +52,28 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( + + $isLast = $node->cond->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$exprType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Elseif condition is always %s.', - $exprType->getValue() ? 'true' : 'false' - )))->line($node->cond->getLine()) - ->identifier('deadCode.elseifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), - ]; + $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 f597146def..a4b08ef043 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -2,36 +2,40 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class IfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.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,22 +49,20 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'If condition is always %s.', - $exprType->getValue() ? 'true' : 'false' - )))->line($node->cond->getLine()) - ->identifier('deadCode.ifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + $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 04a697c237..9abb0b528c 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,34 +4,35 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +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, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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 +42,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 +56,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 +68,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->getArgs()) - )))->build(), - ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to true.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()) - )))->build(), + $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 dd36fee1d5..7d6585ebaf 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -2,79 +2,91 @@ 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\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\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; + +#[AutowiredService] +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, + #[AutowiredParameter] + private array $universalObjectCratesClasses, + #[AutowiredParameter] + 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->getArgs()) > 0 - ) { - if ($node->name instanceof \PhpParser\Node\Name) { + if ($node instanceof FuncCall) { + if ($node->isFirstClassCallable()) { + return null; + } + $args = $node->getArgs(); + $argsCount = count($args); + if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); - if ($functionName === 'assert') { - $assertValue = $scope->getType($node->getArgs()[0]->value)->toBoolean(); - if (!$assertValue instanceof ConstantBooleanType) { + if ($functionName === 'assert' && $argsCount >= 1) { + $arg = $args[0]->value; + $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); + $assertValueIsTrue = $assertValue->isTrue()->yes(); + if (! $assertValueIsTrue && ! $assertValue->isFalse()->yes()) { return null; } - return $assertValue->getValue(); + return $assertValueIsTrue; } if (in_array($functionName, [ 'class_exists', 'interface_exists', 'trait_exists', + 'enum_exists', ], true)) { return null; } @@ -82,11 +94,11 @@ public function findSpecifiedType( return null; } elseif ($functionName === 'defined') { return null; - } elseif ( - $functionName === 'in_array' - && count($node->getArgs()) >= 3 - ) { - $haystackType = $scope->getType($node->getArgs()[1]->value); + } elseif ($functionName === 'array_search') { + return null; + } elseif ($functionName === 'in_array' && $argsCount >= 2) { + $haystackArg = $args[1]->value; + $haystackType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($haystackArg) : $scope->getNativeType($haystackArg)); if ($haystackType instanceof MixedType) { return null; } @@ -95,22 +107,73 @@ public function findSpecifiedType( return null; } - if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { - $needleType = $scope->getType($node->getArgs()[0]->value); + $needleArg = $args[0]->value; + $needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg)); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($args[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 (count($haystackArrayValueType->getFiniteTypes()) > 1 || $haystackArrayType->isOptionalKey($i)) { + continue; + } + + $haystackArrayValueConstantScalarTypes = $haystackArrayValueType->getConstantScalarTypes(); + if (count($haystackArrayValueConstantScalarTypes) > 1) { + 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; + } } } @@ -119,22 +182,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->getArgs()) >= 2) { - $objectType = $scope->getType($node->getArgs()[0]->value); - $methodType = $scope->getType($node->getArgs()[1]->value); + } elseif ($functionName === 'method_exists' && $argsCount >= 2) { + $objectArg = $args[0]->value; + $objectType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg)); if ($objectType instanceof ConstantStringType && !$this->reflectionProvider->hasClass($objectType->getValue()) @@ -142,12 +202,15 @@ public function findSpecifiedType( return false; } + $methodArg = $args[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; } @@ -156,35 +219,91 @@ 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 { - if ($expr === $node) { - return true; + $rootExpr = $specifiedTypes->getRootExpr(); + if ($rootExpr !== null) { + if (self::isSpecified($typeSpecifierScope, $node, $rootExpr)) { + return null; } - if ($expr instanceof Expr\Variable && is_string($expr->name) && !$scope->hasVariableType($expr->name)->yes()) { - return true; + $rootExprType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr)); + if ($rootExprType instanceof ConstantBooleanType) { + return $rootExprType->getValue(); } - return ( - $node instanceof FuncCall - || $node instanceof MethodCall - || $node instanceof Expr\StaticCall - ) && $scope->isSpecified($expr); - }; + return null; + } - if (count($sureTypes) === 1 && count($sureNotTypes) === 0) { - $sureType = reset($sureTypes); - if ($isSpecified($sureType[0])) { - return null; + $results = []; + + $assignedInCallVars = []; + if ($node instanceof Expr\CallLike) { + foreach ($node->getArgs() as $arg) { + $expr = $arg->value; + while ($expr instanceof Expr\Assign) { + $expr = $expr->expr; + } + $assignedExpr = $expr; + + $expr = $arg->value; + while ($expr instanceof Expr\Assign) { + $assignedInCallVars[] = new Expr\Assign( + $expr->var, + $assignedExpr, + $expr->getAttributes(), + ); + + $expr = $expr->expr; + } + } + } + foreach ($sureTypes as $sureType) { + if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { + $results[] = TrinaryLogic::createMaybe(); + continue; } if ($this->treatPhpDocTypesAsCertain) { @@ -193,21 +312,24 @@ public function findSpecifiedType( $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; + foreach ($assignedInCallVars as $assignedInCallVar) { + if ($sureType[0] !== $assignedInCallVar->var) { + continue; + } + + $argumentType = $scope->getType($assignedInCallVar->expr); } - return null; - } elseif (count($sureNotTypes) === 1 && count($sureTypes) === 0) { - $sureNotType = reset($sureNotTypes); - if ($isSpecified($sureNotType[0])) { - return null; + $results[] = $resultType->isSuperTypeOf($argumentType)->result; + } + + foreach ($sureNotTypes as $sureNotType) { + if (self::isSpecified($typeSpecifierScope, $node, $sureNotType[0])) { + $results[] = TrinaryLogic::createMaybe(); + continue; } if ($this->treatPhpDocTypesAsCertain) { @@ -216,67 +338,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)); @@ -287,7 +400,7 @@ public function getArgumentsDescription( return sprintf( ' with arguments %s and %s', implode(', ', $descriptions), - $lastDescription + $lastDescription, ); } @@ -301,8 +414,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 cce1721459..9935a164f3 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -5,35 +5,37 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +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, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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 +58,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,34 +72,42 @@ 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->getArgs()) - )))->build(), - ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $method = $this->getMethod($node->var, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to method %s::%s()%s will always evaluate to true.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()) - )))->build(), + $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); $method = $scope->getMethodReflection($calledOnType, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 39b91a3b78..3919824104 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -5,35 +5,37 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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\StaticCall> + * @implements Rule */ -class ImpossibleCheckTypeStaticMethodCallRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 4)] +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, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + 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 @@ -56,8 +58,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) { @@ -68,36 +73,40 @@ 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->getArgs()) - )))->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->getArgs()) - )))->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) { @@ -108,7 +117,7 @@ private function getMethod( $method = $scope->getMethodReflection($calledOnType, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php new file mode 100644 index 0000000000..751280bf87 --- /dev/null +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -0,0 +1,115 @@ + + */ +#[RegisteredRule(level: 4)] +final class LogicalXorConstantConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return LogicalXor::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $leftType = $this->helper->getBooleanType($scope, $node->left); + if ($leftType instanceof ConstantBooleanType) { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of xor is always %s.', + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.leftAlways%s', $leftType->getValue() ? 'True' : 'False')) + ->line($node->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + $rightType = $this->helper->getBooleanType($scope, $node->right); + if ($rightType instanceof ConstantBooleanType) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType( + $scope, + $node->right, + ); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of xor is always %s.', + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.rightAlways%s', $rightType->getValue() ? 'True' : 'False')) + ->line($node->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index 51db3018db..8c31b92242 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -4,25 +4,36 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MatchExpressionNode; +use PHPStan\Parser\TryCatchTypeVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use UnhandledMatchError; +use function array_map; +use function count; +use function sprintf; /** * @implements Rule */ -class MatchExpressionRule implements Rule +#[RegisteredRule(level: 4)] +final class MatchExpressionRule implements Rule { - private bool $checkAlwaysTrueStrictComparison; - - public function __construct(bool $checkAlwaysTrueStrictComparison) + public function __construct( + private ConstantConditionRuleHelper $constantConditionRuleHelper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) { - $this->checkAlwaysTrueStrictComparison = $checkAlwaysTrueStrictComparison; } public function getNodeType(): string @@ -33,13 +44,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $matchCondition = $node->getCondition(); - $nextArmIsDead = false; + $matchConditionType = $scope->getType($matchCondition); + $nextArmIsDeadForType = false; + $nextArmIsDeadForNativeType = false; $errors = []; $armsCount = count($node->getArms()); $hasDefault = false; foreach ($node->getArms() as $i => $arm) { - if ($nextArmIsDead) { - $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.')->line($arm->getLine())->build(); + if ( + $nextArmIsDeadForNativeType + || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) + ) { continue; } $armConditions = $arm->getConditions(); @@ -50,48 +65,112 @@ public function processNode(Node $node, Scope $scope): array $armConditionScope = $armCondition->getScope(); $armConditionExpr = new Node\Expr\BinaryOp\Identical( $matchCondition, - $armCondition->getCondition() + $armCondition->getCondition(), ); + $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { 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)->build(); - } else { - $nextArmIsDead = true; - if ( - $this->checkAlwaysTrueStrictComparison - && ($i !== $armsCount - 1 || $i === 0) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Match arm comparison between %s and %s is always true.', - $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()) - ))->line($armLine)->build(); - } + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + ))->line($armLine)->identifier('match.alwaysFalse')->build(); + continue; } + + if ($i === $armsCount - 1) { + continue; + } + + $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()), + ); + $errors[] = RuleErrorBuilder::message($message) + ->line($armLine) + ->identifier('match.alwaysTrue') + ->tip('Remove remaining cases below this one and this error will disappear too.') + ->build(); } } - if (!$hasDefault && !$nextArmIsDead) { + if (!$hasDefault && !$nextArmIsDeadForType) { $remainingType = $node->getEndScope()->getType($matchCondition); - if (!$remainingType instanceof NeverType) { + $cases = $remainingType->getEnumCases(); + $casesCount = count($cases); + if ($casesCount > 1) { + $remainingType = new UnionType($cases); + } + if ($casesCount === 1) { + $remainingType = $cases[0]; + } + if ( + !$remainingType instanceof NeverType + && !$this->isUnhandledMatchErrorCaught($node) + && !$this->hasUnhandledMatchErrorThrowsTag($scope) + ) { $errors[] = RuleErrorBuilder::message(sprintf( 'Match expression does not handle remaining %s: %s', $remainingType instanceof UnionType ? 'values' : 'value', - $remainingType->describe(VerbosityLevel::value()) - ))->build(); + $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..14bf2c5a6d 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -2,25 +2,43 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class NumberComparisonOperatorsConstantConditionRule implements Rule { + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.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 +50,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..f0e71a0ee9 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,22 +3,38 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class StrictComparisonOfDifferentTypesRule implements Rule { - private bool $checkAlwaysTrueStrictComparison; - - public function __construct(bool $checkAlwaysTrueStrictComparison) + public function __construct( + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, + ) { - $this->checkAlwaysTrueStrictComparison = $checkAlwaysTrueStrictComparison; } public function getNodeType(): string @@ -28,39 +44,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 5e1e96b3a9..b6c515a4f3 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -2,36 +2,40 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +final class TernaryOperatorConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.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,23 +49,17 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Ternary operator condition is always %s.', - $exprType->getValue() ? 'true' : 'false' - ))) - ->identifier('deadCode.ternaryConstantCondition') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + $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 e26b63d748..0000000000 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ /dev/null @@ -1,85 +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())) - ->identifier('deadCode.unreachableElseif') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - 'depth' => $elseif->getAttribute('statementDepth'), - 'order' => $elseif->getAttribute('statementOrder'), - ]) - ->build(); - continue; - } - - $condition = $elseif->cond; - $conditionType = $scope->getType($condition)->toBoolean(); - $nextBranchIsDead = $conditionType instanceof ConstantBooleanType && $conditionType->getValue() && $this->helper->shouldSkip($scope, $elseif->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($elseif->cond); - } - - if ($node->else !== null && $nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Else branch is unreachable because previous condition is always true.'))->line($node->else->getLine()) - ->identifier('deadCode.unreachableElse') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - ]) - ->build(); - } - - return $errors; - } - -} diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php deleted file mode 100644 index 2c9a217bf0..0000000000 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ /dev/null @@ -1,73 +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()) - ->identifier('deadCode.unreachableTernaryElse') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ]) - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php index 8494d0c2b2..6016e72515 100644 --- a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php @@ -4,14 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VoidType; /** * @implements Rule */ -class UsageOfVoidMatchExpressionRule implements Rule +#[RegisteredRule(level: 2)] +final class UsageOfVoidMatchExpressionRule implements Rule { public function getNodeType(): string @@ -21,12 +22,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $matchResultType = $scope->getType($node); - if ( - $matchResultType instanceof VoidType - && !$scope->isInFirstLevelStatement() - ) { - return [RuleErrorBuilder::message('Result of match expression (void) is used.')->build()]; + if (!$scope->isInFirstLevelStatement()) { + $matchResultType = $scope->getKeepVoidType($node); + if ($matchResultType->isVoid()->yes()) { + return [RuleErrorBuilder::message('Result of match expression (void) is used.')->identifier('match.void')->build()]; + } } return []; diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index f9288e90f5..1057d4d3d7 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -2,27 +2,30 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Stmt\While_; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class WhileLoopAlwaysFalseConditionRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 4)] +final class WhileLoopAlwaysFalseConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -31,12 +34,12 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); - if ($exprType instanceof ConstantBooleanType && !$exprType->getValue()) { + if ($exprType->isFalse()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -46,12 +49,16 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') ->build(), ]; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index ebb5325d31..86d0f01f2c 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -2,30 +2,33 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\BreaklessWhileLoopNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class WhileLoopAlwaysTrueConditionRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 4)] +final class WhileLoopAlwaysTrueConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -34,8 +37,8 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { foreach ($node->getExitPoints() as $exitPoint) { @@ -49,7 +52,7 @@ public function processNode( if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -63,7 +66,7 @@ public function processNode( } $originalNode = $node->getOriginalNode(); $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); - if ($exprType instanceof ConstantBooleanType && $exprType->getValue()) { + if ($exprType->isTrue()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -73,12 +76,16 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') ->build(), ]; } diff --git a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php index 756cd130bf..977bac234b 100644 --- a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php +++ b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php @@ -2,12 +2,29 @@ namespace PHPStan\Rules\Constants; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ClassConstantReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-used class constant. + * + * To register it in the configuration file use the `phpstan.constants.alwaysUsedClassConstantsExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.constants.alwaysUsedClassConstantsExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-used-class-constants + * + * @api + */ interface AlwaysUsedClassConstantsExtension { - public function isAlwaysUsed(ConstantReflection $constant): bool; + public function isAlwaysUsed(ClassConstantReflection $constant): bool; } diff --git a/src/Rules/Constants/ClassAsClassConstantRule.php b/src/Rules/Constants/ClassAsClassConstantRule.php new file mode 100644 index 0000000000..35574b4afc --- /dev/null +++ b/src/Rules/Constants/ClassAsClassConstantRule.php @@ -0,0 +1,42 @@ + + */ +#[RegisteredRule(level: 0)] +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 8770d20c2f..bfb35fa6f4 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -4,14 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 1)] +final class ConstantRule implements Rule { + public function __construct( + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, + ) + { + } + public function getNodeType(): string { return Node\Expr\ConstFetch::class; @@ -20,11 +32,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->hasConstant($node->name)) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Constant %s not found.', + (string) $node->name, + )) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf( - 'Constant %s not found.', - (string) $node->name - ))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Constants/DynamicClassConstantFetchRule.php b/src/Rules/Constants/DynamicClassConstantFetchRule.php new file mode 100644 index 0000000000..62046c1b50 --- /dev/null +++ b/src/Rules/Constants/DynamicClassConstantFetchRule.php @@ -0,0 +1,71 @@ + + */ +#[RegisteredRule(level: 0)] +final class DynamicClassConstantFetchRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Expr) { + return []; + } + + if (!$this->phpVersion->supportsDynamicClassConstantFetch()) { + return [ + RuleErrorBuilder::message('Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.') + ->identifier('classConstant.dynamicFetch') + ->nonIgnorable() + ->build(), + ]; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type): bool => $type->isString()->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return []; + } + if ($type->isString()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Class constant name in dynamic fetch can only be a string, %s given.', + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.nameType')->build(), + ]; + } + +} diff --git a/src/Rules/Constants/FinalConstantRule.php b/src/Rules/Constants/FinalConstantRule.php index ffbcc7d491..6a72e67e1f 100644 --- a/src/Rules/Constants/FinalConstantRule.php +++ b/src/Rules/Constants/FinalConstantRule.php @@ -5,19 +5,18 @@ use PhpParser\Node; use PhpParser\Node\Stmt\ClassConst; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** @implements Rule */ -class FinalConstantRule implements Rule +#[RegisteredRule(level: 0)] +final class FinalConstantRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -36,7 +35,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.') + ->identifier('classConstant.finalNotSupported') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Constants/FinalPrivateConstantRule.php b/src/Rules/Constants/FinalPrivateConstantRule.php new file mode 100644 index 0000000000..1b8360ef14 --- /dev/null +++ b/src/Rules/Constants/FinalPrivateConstantRule.php @@ -0,0 +1,51 @@ + */ +#[RegisteredRule(level: 0)] +final class FinalPrivateConstantRule implements Rule +{ + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $classReflection = $scope->getClassReflection(); + + if (!$node->isFinal()) { + return []; + } + + if (!$node->isPrivate()) { + return []; + } + + $errors = []; + foreach ($node->consts as $classConstNode) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s() cannot be final as it is never overridden by other classes.', + $classReflection->getDisplayName(), + $classConstNode->name->name, + ))->identifier('classConstant.finalPrivate')->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php index 80a5424321..cc325cdc33 100644 --- a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php +++ b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php @@ -2,28 +2,23 @@ namespace PHPStan\Rules\Constants; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider +#[AutowiredService] +final class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { - private Container $container; - /** @var AlwaysUsedClassConstantsExtension[]|null */ private ?array $extensions = null; - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getExtensions(): array { - if ($this->extensions === null) { - $this->extensions = $this->container->getServicesByTag(AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG); - } - - return $this->extensions; + return $this->extensions ??= $this->container->getServicesByTag(AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG); } } diff --git a/src/Rules/Constants/MagicConstantContextRule.php b/src/Rules/Constants/MagicConstantContextRule.php new file mode 100644 index 0000000000..0c5cb195dc --- /dev/null +++ b/src/Rules/Constants/MagicConstantContextRule.php @@ -0,0 +1,77 @@ + */ +#[RegisteredRule(level: 0)] +final class MagicConstantContextRule implements Rule +{ + + public function getNodeType(): string + { + return MagicConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // test cases https://3v4l.org/ZUvvr + + if ($node instanceof MagicConst\Class_) { + if ($scope->isInClass()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a class.', $node->getName()), + )->identifier('magicConstant.outOfClass')->build(), + ]; + } elseif ($node instanceof MagicConst\Trait_) { + if ($scope->isInTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a trait.', $node->getName()), + )->identifier('magicConstant.outOfTrait')->build(), + ]; + } elseif ($node instanceof MagicConst\Method || $node instanceof MagicConst\Function_) { + if ($scope->getFunctionName() !== null) { + return []; + } + if ($scope->isInAnonymousFunction()) { + return []; + } + + if ((bool) $node->getAttribute(MagicConstantParamDefaultVisitor::ATTRIBUTE_NAME)) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a function.', $node->getName()), + )->identifier('magicConstant.outOfFunction')->build(), + ]; + } elseif ($node instanceof MagicConst\Namespace_) { + if ($scope->getNamespace() === null) { + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty in global namespace.', $node->getName()), + )->identifier('magicConstant.outOfNamespace')->build(), + ]; + } + } + return []; + } + +} diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index f28cd71a76..bb2d10164b 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -4,23 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingClassConstantTypehintRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 6)] +final class MissingClassConstantTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -31,7 +34,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $errors = []; @@ -44,13 +47,15 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $constantName - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - $constantType = $constantReflection->getValueType(); + $constantType = $constantReflection->getPhpDocType(); + if ($constantType === null) { + return []; + } $errors = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { @@ -59,8 +64,11 @@ private function processSingleConstant(ClassReflection $classReflection, string 'Constant %s::%s type has no value type specified in iterable type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $iterableTypeDescription - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($constantType) as [$name, $genericTypeNames]) { @@ -69,8 +77,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $name, - implode(', ', $genericTypeNames) - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($constantType) as $callableType) { @@ -78,8 +88,8 @@ private function processSingleConstant(ClassReflection $classReflection, string 'Constant %s::%s type has no signature specified for %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $callableType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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..ab00605542 --- /dev/null +++ b/src/Rules/Constants/NativeTypedClassConstantRule.php @@ -0,0 +1,46 @@ + + */ +#[RegisteredRule(level: 0)] +final class NativeTypedClassConstantRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->type === null) { + return []; + } + + if ($this->phpVersion->supportsNativeTypesInClassConstants()) { + return []; + } + + return [ + RuleErrorBuilder::message('Class constants with native types are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index 34f9bf424e..78cb6faff7 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -4,27 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * @implements Rule */ -class OverridingConstantRule implements Rule +#[RegisteredRule(level: 0)] +final class OverridingConstantRule implements Rule { - private bool $checkPhpDocMethodSignatures; - public function __construct( - bool $checkPhpDocMethodSignatures + #[AutowiredParameter] + private bool $checkPhpDocMethodSignatures, ) { - $this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; } public function getNodeType(): string @@ -35,7 +38,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $errors = []; @@ -48,21 +51,16 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $constantName - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $prototype = $this->findPrototype($classReflection, $constantName); - if (!$prototype instanceof ClassConstantReflection) { + if ($prototype === null) { return []; } $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $errors = []; if ($prototype->isFinal()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -70,8 +68,8 @@ private function processSingleConstant(ClassReflection $classReflection, string $classReflection->getDisplayName(), $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototype->getName(), + ))->identifier('classConstant.final')->nonIgnorable()->build(); } if ($prototype->isPublic()) { @@ -82,8 +80,8 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototype->getName(), + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } } elseif ($constantReflection->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -91,14 +89,42 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); + $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; } @@ -115,14 +141,14 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getValueType()->describe(VerbosityLevel::value()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->build(); + $prototype->getName(), + ))->identifier('classConstant.type')->build(); } return $errors; } - private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection + private function findPrototype(ClassReflection $classReflection, string $constantName): ?ClassConstantReflection { foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { if ($immediateInterface->hasConstant($constantName)) { diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php new file mode 100644 index 0000000000..9041a60ef0 --- /dev/null +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -0,0 +1,130 @@ + + */ +#[RegisteredRule(level: 2)] +final class ValueAssignedToClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant( + $scope->getClassReflection(), + $constantName, + $scope->getType($const->value), + $nativeType, + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName, Type $valueExprType, ?Type $nativeType): array + { + $constantReflection = $classReflection->getConstant($constantName); + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { + if ($nativeType === null) { + return []; + } + + $accepts = $nativeType->accepts($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $nativeType->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe(VerbosityLevel::value()), + ))->acceptsReasonsTip($accepts->reasons)->nonIgnorable()->identifier('classConstant.value')->build(), + ]; + } elseif ($nativeType === null) { + $isSuperType = $phpDocType->isSuperTypeOf($valueExprType); + $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $valueExprType); + if ($isSuperType->no()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + + } elseif ($isSuperType->maybe()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + } + + return []; + } + + $type = $constantReflection->getValueType(); + $accepts = $type->accepts($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($type, $valueExprType); + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $type->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('classConstant.value')->build(), + ]; + } + +} diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php index be5572d975..360d6dccd2 100644 --- a/src/Rules/DateTimeInstantiationRule.php +++ b/src/Rules/DateTimeInstantiationRule.php @@ -6,12 +6,18 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\DependencyInjection\RegisteredRule; +use Throwable; +use function count; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class DateTimeInstantiationRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 5)] +final class DateTimeInstantiationRule implements Rule { public function getNodeType(): string @@ -24,35 +30,40 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ( - !($node->class instanceof \PhpParser\Node\Name) - || \count($node->getArgs()) === 0 - || !\in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) - ) { + if (!$node->class instanceof Node\Name) { return []; } - $arg = $scope->getType($node->getArgs()[0]->value); - if (!($arg instanceof ConstantStringType)) { + $lowerClassName = strtolower((string) $node->class); + if ( + count($node->getArgs()) === 0 + || !in_array($lowerClassName, ['datetime', 'datetimeimmutable'], true) + ) { return []; } + $arg = $scope->getType($node->getArgs()[0]->value); $errors = []; - $dateString = $arg->getValue(); - try { - new DateTime($dateString); - } catch (\Throwable $e) { - // an exception is thrown for errors only but we want to catch warnings too - } - $lastErrors = DateTime::getLastErrors(); - if ($lastErrors !== false) { + + foreach ($arg->getConstantStrings() as $constantString) { + $dateString = $constantString->getValue(); + try { + new DateTime($dateString); + } catch (Throwable) { + // an exception is thrown for errors only but we want to catch warnings too + } + $lastErrors = DateTime::getLastErrors(); + if ($lastErrors === false) { + continue; + } + foreach ($lastErrors['errors'] as $error) { $errors[] = RuleErrorBuilder::message(sprintf( 'Instantiating %s with %s produces an error: %s', - (string) $node->class, + $lowerClassName === 'datetime' ? 'DateTime' : 'DateTimeImmutable', $dateString, - $error - ))->build(); + $error, + ))->identifier(sprintf('new.%s', $lowerClassName === 'datetime' ? 'dateTime' : 'dateTimeImmutable'))->build(); } } diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..71673e8e43 --- /dev/null +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -0,0 +1,56 @@ + + */ +#[RegisteredRule(level: 4)] +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..06d710a83a --- /dev/null +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -0,0 +1,56 @@ + + */ +#[RegisteredRule(level: 4)] +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..a04d151ad9 --- /dev/null +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,69 @@ + + */ +#[RegisteredRule(level: 4)] +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); + $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..f8938547b5 --- /dev/null +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,66 @@ + + */ +#[RegisteredRule(level: 4)] +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); + $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..86d93e1c4a --- /dev/null +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -0,0 +1,58 @@ + + */ +#[RegisteredCollector(level: 4)] +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..d7abebe3eb --- /dev/null +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -0,0 +1,57 @@ + + */ +#[RegisteredCollector(level: 4)] +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..776e55969c --- /dev/null +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -0,0 +1,61 @@ + + */ +#[RegisteredCollector(level: 4)] +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 fa7a42c9dd..4a8cb110bd 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -3,66 +3,136 @@ namespace PHPStan\Rules\DeadCode; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 4)] +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\NullsafePropertyFetch - && !$expr instanceof Node\Expr\ArrayDimFetch - && !$expr instanceof Node\Scalar - && !$expr instanceof Node\Expr\Isset_ - && !$expr instanceof Node\Expr\Empty_ - && !$expr instanceof Node\Expr\ConstFetch - && !$expr instanceof Node\Expr\ClassConstFetch + $expr instanceof Node\Expr\Assign + || $expr instanceof Node\Expr\AssignOp + || $expr instanceof Node\Expr\AssignRef ) { return []; } + if ($expr instanceof Node\Expr\Closure) { + return []; + } + + $exprString = $this->exprPrinter->printExpr($expr); + $exprStringLines = preg_split('~\R~', $exprString, 2); + if ($exprStringLines !== false && count($exprStringLines) > 1) { + $exprString = $exprStringLines[0] . '…'; + } + return [ RuleErrorBuilder::message(sprintf( 'Expression "%s" on a separate line does not do anything.', - $this->printer->prettyPrintExpr($originalExpr) - ))->line($expr->getLine()) - ->identifier('deadCode.noopExpression') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + $exprString, + ))->line($expr->getStartLine()) + ->identifier('expr.resultUnused') ->build(), ]; } diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php new file mode 100644 index 0000000000..4147f0b0e0 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -0,0 +1,52 @@ + + */ +#[RegisteredCollector(level: 4)] +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..02c24768c9 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -0,0 +1,76 @@ +, string, int}> + */ +#[RegisteredCollector(level: 4)] +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..54d8e1da7a --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -0,0 +1,62 @@ + + */ +#[RegisteredCollector(level: 4)] +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..b8204335c0 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -0,0 +1,57 @@ + + */ +#[RegisteredCollector(level: 4)] +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 67a7105658..92618e4855 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -4,14 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\UnreachableStatementNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\UnreachableStatementNode> + * @implements Rule */ -class UnreachableStatementRule implements Rule +#[RegisteredRule(level: 4)] +final class UnreachableStatementRule implements Rule { public function getNodeType(): string @@ -21,17 +23,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->getOriginalStatement() instanceof Node\Stmt\Nop) { - return []; - } - return [ RuleErrorBuilder::message('Unreachable statement - code above always terminates.') - ->identifier('deadCode.unreachableStatement') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + ->identifier('deadCode.unreachable') ->build(), ]; } diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 6b3006f87f..6e017873d2 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -4,22 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassConstantsNode; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectType; +use function sprintf; /** * @implements Rule */ -class UnusedPrivateConstantRule implements Rule +#[RegisteredRule(level: 4)] +final class UnusedPrivateConstantRule implements Rule { - private AlwaysUsedClassConstantsExtensionProvider $extensionProvider; - - public function __construct(AlwaysUsedClassConstantsExtensionProvider $extensionProvider) + public function __construct(private AlwaysUsedClassConstantsExtensionProvider $extensionProvider) { - $this->extensionProvider = $extensionProvider; } public function getNodeType(): string @@ -29,14 +30,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->getClass() instanceof Node\Stmt\Class_) { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { @@ -60,31 +59,46 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getFetches() as $fetch) { $fetchNode = $fetch->getNode(); - if (!$fetchNode->class instanceof Node\Name) { - continue; + + $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; } - $fetchScope = $fetch->getScope(); - $fetchedOnClass = $fetchScope->resolveName($fetchNode->class); - if ($fetchedOnClass !== $classReflection->getName()) { + + $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->getLine()) - ->identifier('deadCode.unusedClassConstant') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'constantName' => $constantName, - ]) + ->line($constantNode->getStartLine()) + ->identifier('classConstant.unused') + ->tip(sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-used-class-constants')) ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 4d3c3cb2fb..2c1ab6ce64 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -5,22 +5,30 @@ use PhpParser\Node; use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassMethodsNode; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; +use function array_map; +use function count; +use function sprintf; +use function strtolower; /** * @implements Rule */ -class UnusedPrivateMethodRule implements Rule +#[RegisteredRule(level: 4)] +final class UnusedPrivateMethodRule implements Rule { + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + public function getNodeType(): string { return ClassMethodsNode::class; @@ -28,32 +36,40 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->getClass() instanceof Node\Stmt\Class_) { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { $constructor = $classReflection->getConstructor(); } - $classType = new ObjectType($classReflection->getName()); $methods = []; foreach ($node->getMethods() as $method) { - if (!$method->isPrivate()) { + if (!$method->getNode()->isPrivate()) { + continue; + } + if ($method->isDeclaredInTrait()) { continue; } - $methodName = $method->name->toString(); + $methodName = $method->getNode()->name->toString(); if ($constructor !== null && $constructor->getName() === $methodName) { continue; } if (strtolower($methodName) === '__clone') { continue; } - $methods[$method->name->toString()] = $method; + + $methodReflection = $classReflection->getNativeMethod($methodName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($methodReflection)) { + continue 2; + } + } + + $methods[strtolower($methodName)] = $method; } $arrayCalls = []; @@ -68,40 +84,56 @@ public function processNode(Node $node, Scope $scope): array $methodNames = [$methodCallNode->name->toString()]; } else { $methodNameType = $callScope->getType($methodCallNode->name); - $strings = TypeUtils::getConstantStrings($methodNameType); + $strings = $methodNameType->getConstantStrings(); if (count($strings) === 0) { - return []; + // handle subtractions of a dynamic method call + foreach ($methods as $lowerMethodName => $method) { + if ((new ConstantStringType($method->getNode()->name->toString()))->isSuperTypeOf($methodNameType)->no()) { + continue; + } + + unset($methods[$lowerMethodName]); + } + + continue; } - $methodNames = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, $strings); + $methodNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } if ($methodCallNode instanceof Node\Expr\MethodCall) { $calledOnType = $callScope->getType($methodCallNode->var); } else { - if (!$methodCallNode->class instanceof Node\Name) { - continue; + if ($methodCallNode->class instanceof Node\Name) { + $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); + } else { + $calledOnType = $callScope->getType($methodCallNode->class); } - $calledOnType = $scope->resolveTypeByName($methodCallNode->class); - } - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; } + $inMethod = $callScope->getFunction(); if (!$inMethod instanceof MethodReflection) { continue; } foreach ($methodNames as $methodName) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } if ($inMethod->getName() === $methodName) { continue; } - unset($methods[$methodName]); + unset($methods[strtolower($methodName)]); } } @@ -110,53 +142,52 @@ public function processNode(Node $node, Scope $scope): array /** @var Node\Expr\Array_ $array */ $array = $arrayCall->getNode(); $arrayScope = $arrayCall->getScope(); - $arrayType = $scope->getType($array); - if (!$arrayType instanceof ConstantArrayType) { - continue; - } - $typeAndMethod = $arrayType->findTypeAndMethodName(); - if ($typeAndMethod === null) { - continue; - } - if ($typeAndMethod->isUnknown()) { - return []; - } - if (!$typeAndMethod->getCertainty()->yes()) { - return []; - } - $calledOnType = $typeAndMethod->getType(); - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; - } - $inMethod = $arrayScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { + $arrayType = $arrayScope->getType($array); + if (!$arrayType->isCallable()->yes()) { continue; } - if ($inMethod->getName() === $typeAndMethod->getMethod()) { - continue; + foreach ($arrayType->getConstantArrays() as $constantArray) { + foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethod) { + if ($typeAndMethod->isUnknown()) { + return []; + } + if (!$typeAndMethod->getCertainty()->yes()) { + return []; + } + + $calledOnType = $typeAndMethod->getType(); + $methodReflection = $arrayScope->getMethodReflection($calledOnType, $typeAndMethod->getMethod()); + if ($methodReflection === null) { + continue; + } + + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + + $inMethod = $arrayScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { + continue; + } + if ($inMethod->getName() === $typeAndMethod->getMethod()) { + continue; + } + unset($methods[strtolower($typeAndMethod->getMethod())]); + } } - unset($methods[$typeAndMethod->getMethod()]); } } $errors = []; - foreach ($methods as $methodName => $methodNode) { + foreach ($methods as $method) { + $originalMethodName = $method->getNode()->name->toString(); $methodType = 'Method'; - if ($methodNode->isStatic()) { + if ($method->getNode()->isStatic()) { $methodType = 'Static method'; } - $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $methodName)) - ->line($methodNode->getLine()) - ->identifier('deadCode.unusedMethod') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'methodName' => $methodName, - ]) + $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $originalMethodName)) + ->line($method->getNode()->getStartLine()) + ->identifier('method.unused') ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 6ba7338b28..7d2ccc7328 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -4,48 +4,45 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertiesNode; use PHPStan\Node\Property\PropertyRead; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function array_map; +use function count; +use function is_string; +use function lcfirst; +use function sprintf; +use function str_contains; /** * @implements Rule */ -class UnusedPrivatePropertyRule implements Rule +#[RegisteredRule(level: 4)] +final class UnusedPrivatePropertyRule implements Rule { - private ReadWritePropertiesExtensionProvider $extensionProvider; - - /** @var string[] */ - private array $alwaysWrittenTags; - - /** @var string[] */ - private array $alwaysReadTags; - - private bool $checkUninitializedProperties; - /** - * @param ReadWritePropertiesExtensionProvider $extensionProvider * @param string[] $alwaysWrittenTags * @param string[] $alwaysReadTags */ public function __construct( - ReadWritePropertiesExtensionProvider $extensionProvider, - array $alwaysWrittenTags, - array $alwaysReadTags, - bool $checkUninitializedProperties + private ReadWritePropertiesExtensionProvider $extensionProvider, + #[AutowiredParameter(ref: '%propertyAlwaysWrittenTags%')] + private array $alwaysWrittenTags, + #[AutowiredParameter(ref: '%propertyAlwaysReadTags%')] + private array $alwaysReadTags, + #[AutowiredParameter] + private bool $checkUninitializedProperties, ) { - $this->extensionProvider = $extensionProvider; - $this->alwaysWrittenTags = $alwaysWrittenTags; - $this->alwaysReadTags = $alwaysReadTags; - $this->checkUninitializedProperties = $checkUninitializedProperties; } public function getNodeType(): string @@ -58,24 +55,23 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); - + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $properties = []; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { continue; } + if ($property->isDeclaredInTrait()) { + continue; + } - $alwaysRead = false; - $alwaysWritten = false; + $alwaysRead = !$property->isReadable(); + $alwaysWritten = !$property->isWritable(); if ($property->getPhpDoc() !== null) { $text = $property->getPhpDoc(); foreach ($this->alwaysReadTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -84,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array } foreach ($this->alwaysWrittenTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -122,45 +118,99 @@ public function processNode(Node $node, Scope $scope): array 'read' => $read, 'written' => $written, 'node' => $property, + 'onlyReadable' => $property->isReadable() && !$property->isWritable(), + 'onlyWritable' => $property->isWritable() && !$property->isReadable(), ]; } foreach ($node->getPropertyUsages() as $usage) { + $usageScope = $usage->getScope(); $fetch = $usage->getFetch(); if ($fetch->name instanceof Node\Identifier) { - $propertyNames = [$fetch->name->toString()]; + $propertyName = $fetch->name->toString(); + $propertyNames = [$propertyName]; + if ( + $usageScope->getFunction() !== null + && $fetch instanceof Node\Expr\PropertyFetch + && $fetch->var instanceof Node\Expr\Variable + && is_string($fetch->var->name) + && $fetch->var->name === 'this' + ) { + $methodReflection = $usageScope->getFunction(); + if ( + $methodReflection instanceof PhpMethodFromParserNodeReflection + && $methodReflection->isPropertyHook() + && $methodReflection->getHookedPropertyName() === $propertyName + && ( + $methodReflection->getPropertyHookName() === 'set' + || $usage instanceof PropertyRead + ) + ) { + continue; + } + } } else { - $propertyNameType = $usage->getScope()->getType($fetch->name); - $strings = TypeUtils::getConstantStrings($propertyNameType); + $propertyNameType = $usageScope->getType($fetch->name); + $strings = $propertyNameType->getConstantStrings(); if (count($strings) === 0) { - return []; - } + // handle subtractions of a dynamic property fetch + foreach ($properties as $propertyName => $data) { + if ((new ConstantStringType($propertyName))->isSuperTypeOf($propertyNameType)->no()) { + continue; + } + + unset($properties[$propertyName]); + } - $propertyNames = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, $strings); - } - if ($fetch instanceof Node\Expr\PropertyFetch) { - $fetchedOnType = $usage->getScope()->getType($fetch->var); - } else { - if (!$fetch->class instanceof Node\Name) { continue; } - $fetchedOnType = $usage->getScope()->resolveTypeByName($fetch->class); + $propertyNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { - continue; - } - if ($fetchedOnType instanceof MixedType) { - continue; + 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; } + + $propertyNode = $properties[$propertyName]['node']; + if ($propertyNode->isStatic()) { + $propertyReflection = $usageScope->getStaticPropertyReflection($fetchedOnType, $propertyName); + } else { + $propertyReflection = $usageScope->getInstancePropertyReflection($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 { @@ -169,39 +219,51 @@ public function processNode(Node $node, Scope $scope): array } } - $constructors = []; - $classReflection = $scope->getClassReflection(); - if ($classReflection->hasConstructor()) { - $constructors[] = $classReflection->getConstructor()->getName(); - } - - [$uninitializedProperties] = $node->getUninitializedProperties($scope, $constructors, $this->extensionProvider->getExtensions()); + [$uninitializedProperties] = $node->getUninitializedProperties($scope, []); $errors = []; foreach ($properties as $name => $data) { $propertyNode = $data['node']; if ($propertyNode->isStatic()) { - $propertyName = sprintf('Static property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Static property %s::$%s', $classReflection->getDisplayName(), $name); } else { - $propertyName = sprintf('Property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Property %s::$%s', $classReflection->getDisplayName(), $name); } + $tip = sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-read-written-properties'); if (!$data['read']) { if (!$data['written']) { $errors[] = RuleErrorBuilder::message(sprintf('%s is unused.', $propertyName)) ->line($propertyNode->getStartLine()) - ->identifier('deadCode.unusedProperty') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'propertyName' => $name, - ]) + ->tip($tip) + ->identifier('property.unused') ->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName))->line($propertyNode->getStartLine())->build(); + if ($data['onlyReadable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Readable %s is never read.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverRead') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyWritten') + ->tip($tip) + ->build(); + } } } elseif (!$data['written'] && (!array_key_exists($name, $uninitializedProperties) || !$this->checkUninitializedProperties)) { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName))->line($propertyNode->getStartLine())->build(); + if ($data['onlyWritable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Writable %s is never written.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverWritten') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyRead') + ->tip($tip) + ->build(); + } } } diff --git a/src/Rules/Debug/DebugScopeRule.php b/src/Rules/Debug/DebugScopeRule.php new file mode 100644 index 0000000000..16a06b4a38 --- /dev/null +++ b/src/Rules/Debug/DebugScopeRule.php @@ -0,0 +1,68 @@ + + */ +#[AutowiredService] +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/DumpNativeTypeRule.php b/src/Rules/Debug/DumpNativeTypeRule.php new file mode 100644 index 0000000000..cfb084c4e3 --- /dev/null +++ b/src/Rules/Debug/DumpNativeTypeRule.php @@ -0,0 +1,61 @@ + + */ +#[AutowiredService] +final class DumpNativeTypeRule 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\dumpnativetype') { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Dumped type: %s', + $scope->getNativeType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()), + ), + )->nonIgnorable()->identifier('phpstan.dumpNativeType')->build(), + ]; + } + +} diff --git a/src/Rules/Debug/DumpPhpDocTypeRule.php b/src/Rules/Debug/DumpPhpDocTypeRule.php new file mode 100644 index 0000000000..1180469b0d --- /dev/null +++ b/src/Rules/Debug/DumpPhpDocTypeRule.php @@ -0,0 +1,61 @@ + + */ +#[AutowiredService] +final class DumpPhpDocTypeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider, private Printer $printer) + { + } + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if ($functionName === null) { + return []; + } + + if (strtolower($functionName) !== 'phpstan\dumpphpdoctype') { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Dumped type: %s', + $this->printer->print($scope->getType($node->getArgs()[0]->value)->toPhpDocNode()), + ), + )->nonIgnorable()->identifier('phpstan.dumpPhpDocType')->build(), + ]; + } + +} diff --git a/src/Rules/Debug/DumpTypeRule.php b/src/Rules/Debug/DumpTypeRule.php index 56ae137d61..5a628a95f5 100644 --- a/src/Rules/Debug/DumpTypeRule.php +++ b/src/Rules/Debug/DumpTypeRule.php @@ -4,22 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; +use function strtolower; /** * @implements Rule */ -class DumpTypeRule implements Rule +#[AutowiredService] +final class DumpTypeRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -43,20 +45,16 @@ public function processNode(Node $node, Scope $scope): array } if (count($node->getArgs()) === 0) { - return [ - RuleErrorBuilder::message(sprintf('Missing argument for %s() function call.', $functionName)) - ->nonIgnorable() - ->build(), - ]; + return []; } return [ RuleErrorBuilder::message( sprintf( 'Dumped type: %s', - $scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()) - ) - )->nonIgnorable()->build(), + $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 index 7ca3dc3712..bf5af7456d 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -5,25 +5,30 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\VerbosityLevel; +use function count; +use function is_string; +use function sprintf; /** * @implements Rule */ -class FileAssertRule implements Rule +#[AutowiredService] +final class FileAssertRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private TypeStringResolver $typeStringResolver, + ) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -50,6 +55,10 @@ public function processNode(Node $node, Scope $scope): array return $this->processAssertNativeType($node->getArgs(), $scope); } + if ($function->getName() === 'PHPStan\\Testing\\assertSuperType') { + return $this->processAssertSuperType($node->getArgs(), $scope); + } + if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') { return $this->processAssertVariableCertainty($node->getArgs(), $scope); } @@ -59,8 +68,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Arg[] $args - * @param Scope $scope - * @return RuleError[] + * @return list */ private function processAssertType(array $args, Scope $scope): array { @@ -68,27 +76,32 @@ private function processAssertType(array $args, Scope $scope): array return []; } - $expectedTypeString = $scope->getType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.type') + ->build(), ]; } /** * @param Node\Arg[] $args - * @param Scope $scope - * @return RuleError[] + * @return list */ private function processAssertNativeType(array $args, Scope $scope): array { @@ -96,28 +109,66 @@ private function processAssertNativeType(array $args, Scope $scope): array return []; } - $scope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedTypeString = $scope->getNativeType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getNativeType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected native type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected native type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getNativeType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.nativeType') + ->build(), + ]; + } + + /** + * @param Node\Arg[] $args + * @return list + */ + private function processAssertSuperType(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 super type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + $expressionType = $scope->getType($args[1]->value); + $expectedType = $this->typeStringResolver->resolve($expectedTypeStrings[0]->getValue()); + if ($expectedType->isSuperTypeOf($expressionType)->yes()) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected subtype of %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType->describe(VerbosityLevel::precise()))) + ->nonIgnorable() + ->identifier('phpstan.superType') + ->build(), ]; } /** * @param Node\Arg[] $args - * @param Scope $scope - * @return RuleError[] + * @return list */ private function processAssertVariableCertainty(array $args, Scope $scope): array { @@ -130,6 +181,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('First argument of %s() must be TrinaryLogic call') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -137,6 +189,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -145,6 +198,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -153,35 +207,39 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $args[1]->value; - if (!$variable instanceof Node\Expr\Variable) { - return [ - RuleErrorBuilder::message('Invalid assertVariableCertainty call.') - ->nonIgnorable() - ->build(), - ]; - } - if (!is_string($variable->name)) { + if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) { + $actualCertaintyValue = $scope->hasVariableType($variable->name); + $variableDescription = sprintf('variable $%s', $variable->name); + } elseif ($variable instanceof Node\Expr\ArrayDimFetch && $variable->dim !== null) { + $offset = $scope->getType($variable->dim); + $actualCertaintyValue = $scope->getType($variable->var)->hasOffsetValueType($offset); + $variableDescription = sprintf('offset %s', $offset->describe(VerbosityLevel::precise())); + } else { return [ RuleErrorBuilder::message('Invalid assertVariableCertainty call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } - $actualCertaintyValue = $scope->hasVariableType($variable->name); if ($expectedCertaintyValue->equals($actualCertaintyValue)) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe()))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected %s certainty %s, actual: %s', $variableDescription, $expectedCertaintyValue->describe(), $actualCertaintyValue->describe())) + ->nonIgnorable() + ->identifier('phpstan.variable') + ->build(), ]; } diff --git a/src/Rules/DirectRegistry.php b/src/Rules/DirectRegistry.php new file mode 100644 index 0000000000..8003bef42a --- /dev/null +++ b/src/Rules/DirectRegistry.php @@ -0,0 +1,56 @@ +rules[$rule->getNodeType()][] = $rule; + } + } + + /** + * @template TNodeType of Node + * @param class-string $nodeType + * @return array> + */ + public function getRules(string $nodeType): array + { + if (!isset($this->cache[$nodeType])) { + $parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); + + $rules = []; + foreach ($parentNodeTypes as $parentNodeType) { + foreach ($this->rules[$parentNodeType] ?? [] as $rule) { + $rules[] = $rule; + } + } + + $this->cache[$nodeType] = $rules; + } + + /** + * @var array> $selectedRules + */ + $selectedRules = $this->cache[$nodeType]; + + return $selectedRules; + } + +} diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php new file mode 100644 index 0000000000..f6489f2e87 --- /dev/null +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -0,0 +1,38 @@ + + */ +#[RegisteredRule(level: 0)] +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 index 5e9040e761..f4e2479a2a 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -4,18 +4,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule */ -class CatchWithUnthrownExceptionRule implements Rule +#[RegisteredRule(level: 4)] +final class CatchWithUnthrownExceptionRule implements Rule { + public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.reportUncheckedExceptionDeadCatch%')] + private bool $reportUncheckedExceptionDeadCatch, + ) + { + } + public function getNodeType(): string { return CatchWithUnthrownExceptionNode::class; @@ -26,15 +39,35 @@ public function processNode(Node $node, Scope $scope): array if ($node->getCaughtType() instanceof NeverType) { return [ RuleErrorBuilder::message( - sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())) - )->line($node->getLine())->build(), + 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->getLine())->build(), + 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 13074ff545..3c99cca44a 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -5,32 +5,34 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Catch_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -47,22 +49,35 @@ public function processNode(Node $node, Scope $scope): array if ($scope->isInClassExists($className)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className))->line($class->getLine())->discoveringSymbolsTip()->build(); + + $errorBuilder = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className)) + ->line($class->getStartLine()) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); continue; } $classReflection = $this->reflectionProvider->getClass($className); - if (!$classReflection->isInterface() && !$classReflection->implementsInterface(\Throwable::class)) { - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); - } - - if (!$this->checkClassCaseSensitivity) { - continue; + 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/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php index c2f4a03bf1..70e2c5806e 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -4,45 +4,36 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; +use function count; -class DefaultExceptionTypeResolver implements ExceptionTypeResolver +/** + * @api + */ +#[AutowiredService(as: DefaultExceptionTypeResolver::class)] +final class DefaultExceptionTypeResolver implements ExceptionTypeResolver { - private ReflectionProvider $reflectionProvider; - - /** @var string[] */ - private array $uncheckedExceptionRegexes; - - /** @var string[] */ - private array $uncheckedExceptionClasses; - - /** @var string[] */ - private array $checkedExceptionRegexes; - - /** @var string[] */ - private array $checkedExceptionClasses; - /** - * @param ReflectionProvider $reflectionProvider * @param string[] $uncheckedExceptionRegexes * @param string[] $uncheckedExceptionClasses * @param string[] $checkedExceptionRegexes * @param string[] $checkedExceptionClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - array $uncheckedExceptionRegexes, - array $uncheckedExceptionClasses, - array $checkedExceptionRegexes, - array $checkedExceptionClasses + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%exceptions.uncheckedExceptionRegexes%')] + private array $uncheckedExceptionRegexes, + #[AutowiredParameter(ref: '%exceptions.uncheckedExceptionClasses%')] + private array $uncheckedExceptionClasses, + #[AutowiredParameter(ref: '%exceptions.checkedExceptionRegexes%')] + private array $checkedExceptionRegexes, + #[AutowiredParameter(ref: '%exceptions.checkedExceptionClasses%')] + private array $checkedExceptionClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->uncheckedExceptionRegexes = $uncheckedExceptionRegexes; - $this->uncheckedExceptionClasses = $uncheckedExceptionClasses; - $this->checkedExceptionRegexes = $checkedExceptionRegexes; - $this->checkedExceptionClasses = $checkedExceptionClasses; } public function isCheckedException(string $className, Scope $scope): bool @@ -65,11 +56,7 @@ public function isCheckedException(string $className, Scope $scope): bool $classReflection = $this->reflectionProvider->getClass($className); foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { - if ($classReflection->getName() === $uncheckedExceptionClass) { - return false; - } - - if (!$classReflection->isSubclassOf($uncheckedExceptionClass)) { + if (!$classReflection->is($uncheckedExceptionClass)) { continue; } @@ -99,11 +86,7 @@ private function isCheckedExceptionInternal(string $className): bool $classReflection = $this->reflectionProvider->getClass($className); foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { - if ($classReflection->getName() === $checkedExceptionClass) { - return true; - } - - if (!$classReflection->isSubclassOf($checkedExceptionClass)) { + if (!$classReflection->is($checkedExceptionClass)) { continue; } diff --git a/src/Rules/Exceptions/ExceptionTypeResolver.php b/src/Rules/Exceptions/ExceptionTypeResolver.php index 83af9366d3..5b7ac7e965 100644 --- a/src/Rules/Exceptions/ExceptionTypeResolver.php +++ b/src/Rules/Exceptions/ExceptionTypeResolver.php @@ -4,7 +4,33 @@ use PHPStan\Analyser\Scope; -/** @api */ +/** + * @api + * + * This interface allows you to write custom logic that can dynamically decide + * whether an exception is checked or unchecked type. + * + * Because the interface accepts a Scope, you can ask about the place in the code where + * it's being decided - a file, a namespace or a class name. + * + * There can only be a single ExceptionTypeResolver per project, and you can register it + * in your configuration file like this: + * + * ``` + * services: + * exceptionTypeResolver!: + * class: PHPStan\Rules\Exceptions\ExceptionTypeResolver + * ``` + * + * You can also take advantage of the `PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver` + * by injecting it into the constructor of your ExceptionTypeResolver + * and delegate the logic of the classes and places you don't care about. + * + * DefaultExceptionTypeResolver decides the type of the exception based on configuration + * parameters like `exceptions.uncheckedExceptionClasses` etc. + * + * Learn more: https://phpstan.org/blog/bring-your-exceptions-under-control + */ interface ExceptionTypeResolver { diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index 7e4b175e46..f3ed6e08f8 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -5,21 +5,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class MissingCheckedExceptionInFunctionThrowsRule implements Rule +final class MissingCheckedExceptionInFunctionThrowsRule implements Rule { - private MissingCheckedExceptionInThrowsCheck $check; - - public function __construct(MissingCheckedExceptionInThrowsCheck $check) + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -30,26 +27,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $errors = []; - foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { + foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $functionReflection->getName(), - $className + $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') - ->metadata([ - 'exceptionName' => $className, - 'newCatchPosition' => $newCatchPosition, - 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), - 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), - ]) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index e18c0c1d97..c564711b2f 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -5,21 +5,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class MissingCheckedExceptionInMethodThrowsRule implements Rule +final class MissingCheckedExceptionInMethodThrowsRule implements Rule { - private MissingCheckedExceptionInThrowsCheck $check; - - public function __construct(MissingCheckedExceptionInThrowsCheck $check) + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -30,27 +27,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); $errors = []; - foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { + foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $className + $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') - ->metadata([ - 'exceptionName' => $className, - 'newCatchPosition' => $newCatchPosition, - 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), - 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), - ]) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php new file mode 100644 index 0000000000..d9b7a6b864 --- /dev/null +++ b/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php @@ -0,0 +1,55 @@ + + */ +final class MissingCheckedExceptionInPropertyHookThrowsRule implements Rule +{ + + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + + if (!$hookReflection->isPropertyHook()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($this->check->check($hookReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $className, + )) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php index 3510d86965..62de7f9e2a 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -4,28 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\ThrowPoint; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\TrinaryLogic; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; +use Throwable; -class MissingCheckedExceptionInThrowsCheck +#[AutowiredService] +final class MissingCheckedExceptionInThrowsCheck { - private ExceptionTypeResolver $exceptionTypeResolver; - - public function __construct(ExceptionTypeResolver $exceptionTypeResolver) + public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + ) { - $this->exceptionTypeResolver = $exceptionTypeResolver; } /** - * @param Type|null $throwType * @param ThrowPoint[] $throwPoints - * @return array + * @return array */ public function check(?Type $throwType, array $throwPoints): array { @@ -40,69 +42,26 @@ public function check(?Type $throwType, array $throwPoints): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ($throwPointType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { + if ($throwPointType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { continue; } if ($throwType->isSuperTypeOf($throwPointType)->yes()) { continue; } - if ( - $throwPointType instanceof TypeWithClassName - && !$this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - ) { + $isCheckedException = TrinaryLogic::createNo()->lazyOr( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->no()) { continue; } - $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode(), $this->getNewCatchPosition($throwPointType, $throwPoint->getNode())]; + $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode()]; } } return $classes; } - private function getNewCatchPosition(Type $throwPointType, Node $throwPointNode): ?int - { - if ($throwPointType instanceof TypeWithClassName) { - // to get rid of type subtraction - $throwPointType = new ObjectType($throwPointType->getClassName()); - } - $tryCatch = $this->findTryCatch($throwPointNode); - if ($tryCatch === null) { - return null; - } - - $position = 0; - foreach ($tryCatch->catches as $catch) { - $type = TypeCombinator::union(...array_map(static function (Node\Name $class): ObjectType { - return new ObjectType($class->toString()); - }, $catch->types)); - if (!$throwPointType->isSuperTypeOf($type)->yes()) { - continue; - } - - $position++; - } - - return $position; - } - - private function findTryCatch(Node $node): ?Node\Stmt\TryCatch - { - if ($node instanceof Node\FunctionLike) { - return null; - } - - if ($node instanceof Node\Stmt\TryCatch) { - return $node; - } - - $parent = $node->getAttribute('parent'); - if ($parent === null) { - return null; - } - - return $this->findTryCatch($parent); - } - } diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php new file mode 100644 index 0000000000..664b1433d1 --- /dev/null +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -0,0 +1,44 @@ + + */ +#[RegisteredRule(level: 0)] +final class NoncapturingCatchRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Catch_::class; + } + + /** + * @param Node\Stmt\Catch_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ($scope->getPhpVersion()->supportsNoncapturingCatches()->yes()) { + return []; + } + + if ($node->var !== null) { + return []; + } + + return [ + RuleErrorBuilder::message('Non-capturing catch is supported only on PHP 8.0 and later.') + ->nonIgnorable() + ->identifier('catch.nonCapturingNotSupported') + ->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php index eeb396aa13..aaaca0956b 100644 --- a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -4,14 +4,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule */ -class OverwrittenExitPointByFinallyRule implements Rule +#[RegisteredRule(level: 4)] +final class OverwrittenExitPointByFinallyRule implements Rule { public function getNodeType(): string @@ -27,11 +31,17 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($node->getTryCatchExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } foreach ($node->getFinallyExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } return $errors; @@ -43,7 +53,7 @@ private function describeExitPoint(Node\Stmt $stmt): string return 'return'; } - if ($stmt instanceof Node\Stmt\Throw_) { + if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Node\Expr\Throw_) { return 'throw'; } diff --git a/src/Rules/Exceptions/ThrowExprTypeRule.php b/src/Rules/Exceptions/ThrowExprTypeRule.php new file mode 100644 index 0000000000..e2b0debb30 --- /dev/null +++ b/src/Rules/Exceptions/ThrowExprTypeRule.php @@ -0,0 +1,64 @@ + + */ +#[RegisteredRule(level: 3)] +final class ThrowExprTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $throwableType = new ObjectType(Throwable::class); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + 'Throwing object of an unknown class %s.', + static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), + ); + + $foundType = $typeResult->getType(); + if ($foundType instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isSuperType = $throwableType->isSuperTypeOf($foundType); + if ($isSuperType->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Invalid type %s to throw.', + $foundType->describe(VerbosityLevel::typeOnly()), + ))->identifier('throw.notThrowable')->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/ThrowExpressionRule.php b/src/Rules/Exceptions/ThrowExpressionRule.php index fd8999066c..ff214c85fd 100644 --- a/src/Rules/Exceptions/ThrowExpressionRule.php +++ b/src/Rules/Exceptions/ThrowExpressionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Parser\StandaloneThrowExprVisitor; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,14 +13,12 @@ /** * @implements Rule */ -class ThrowExpressionRule implements Rule +#[RegisteredRule(level: 0)] +final class ThrowExpressionRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -32,8 +32,14 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->getAttribute(StandaloneThrowExprVisitor::ATTRIBUTE_NAME) === true) { + return []; + } + return [ - RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable() + ->identifier('throw.notSupported') + ->build(), ]; } diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php index b8c3a26e0e..37dee9c0dd 100644 --- a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -4,32 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function sprintf; /** * @implements Rule */ -class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule +#[RegisteredRule(level: 3)] +final class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule { - private ExceptionTypeResolver $exceptionTypeResolver; - - private bool $missingCheckedExceptionInThrows; - public function __construct( - ExceptionTypeResolver $exceptionTypeResolver, - bool $missingCheckedExceptionInThrows + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.check.missingCheckedExceptionInThrows%')] + private bool $missingCheckedExceptionInThrows, ) { - $this->exceptionTypeResolver = $exceptionTypeResolver; - $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; } public function getNodeType(): string @@ -40,12 +38,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); - if (!$functionReflection->getThrowType() instanceof VoidType) { + if ($functionReflection->getThrowType() === null || !$functionReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -56,19 +51,22 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() throws exception %s but the PHPDoc contains @throws void.', $functionReflection->getName(), - $throwPointType->describe(VerbosityLevel::typeOnly()) - ))->line($throwPoint->getNode()->getLine())->build(); + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php index 77f0a1d257..4a97c6c4fc 100644 --- a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -4,32 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function sprintf; /** * @implements Rule */ -class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule +#[RegisteredRule(level: 3)] +final class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule { - private ExceptionTypeResolver $exceptionTypeResolver; - - private bool $missingCheckedExceptionInThrows; - public function __construct( - ExceptionTypeResolver $exceptionTypeResolver, - bool $missingCheckedExceptionInThrows + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.check.missingCheckedExceptionInThrows%')] + private bool $missingCheckedExceptionInThrows, ) { - $this->exceptionTypeResolver = $exceptionTypeResolver; - $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; } public function getNodeType(): string @@ -40,12 +38,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); - if (!$methodReflection->getThrowType() instanceof VoidType) { + if ($methodReflection->getThrowType() === null || !$methodReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -56,11 +51,11 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } @@ -68,8 +63,11 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() throws exception %s but the PHPDoc contains @throws void.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $throwPointType->describe(VerbosityLevel::typeOnly()) - ))->line($throwPoint->getNode()->getLine())->build(); + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php new file mode 100644 index 0000000000..c6cff749d5 --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php @@ -0,0 +1,84 @@ + + */ +#[RegisteredRule(level: 3)] +final class ThrowsVoidPropertyHookWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.check.missingCheckedExceptionInThrows%')] + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + + if ($hookReflection->getThrowType() === null || !$hookReflection->getThrowType()->isVoid()->yes()) { + return []; + } + + if ($hookReflection->getPropertyHookName() === null) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($statementResult->getThrowPoints() as $throwPoint) { + if (!$throwPoint->isExplicit()) { + continue; + } + + foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s throws exception %s but the PHPDoc contains @throws void.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php index a9f6bab81e..6688dec466 100644 --- a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -5,21 +5,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class TooWideFunctionThrowTypeRule implements Rule +final class TooWideFunctionThrowTypeRule implements Rule { - private TooWideThrowTypeCheck $check; - - public function __construct(TooWideThrowTypeCheck $check) + public function __construct(private TooWideThrowTypeCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -30,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $throwType = $functionReflection->getThrowType(); if ($throwType === null) { @@ -45,14 +39,9 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() has %s in PHPDoc @throws tag but it\'s not thrown.', $functionReflection->getName(), - $throwClass + $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index c3f388028c..55c69ee3a5 100644 --- a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -5,25 +5,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; +use function sprintf; /** * @implements Rule */ -class TooWideMethodThrowTypeRule implements Rule +final class TooWideMethodThrowTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private TooWideThrowTypeCheck $check; - - public function __construct(FileTypeMapper $fileTypeMapper, TooWideThrowTypeCheck $check) + public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) { - $this->fileTypeMapper = $fileTypeMapper; - $this->check = $check; } public function getNodeType(): string @@ -33,27 +27,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $docComment = $node->getDocComment(); if ($docComment === null) { return []; } - $classReflection = $scope->getClassReflection(); + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + $classReflection = $node->getClassReflection(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $methodReflection->getName(), - $docComment->getText() + $docComment->getText(), ); if ($resolvedPhpDoc->getThrowsTag() === null) { @@ -68,14 +55,9 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() has %s in PHPDoc @throws tag but it\'s not thrown.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $throwClass + $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php b/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php new file mode 100644 index 0000000000..00ed4bacd4 --- /dev/null +++ b/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php @@ -0,0 +1,74 @@ + + */ +final class TooWidePropertyHookThrowTypeRule implements Rule +{ + + public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + if ($hookReflection->getPropertyHookName() === null) { + throw new ShouldNotHappenException(); + } + + $classReflection = $node->getClassReflection(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $hookReflection->getName(), + $docComment->getText(), + ); + + if ($resolvedPhpDoc->getThrowsTag() === null) { + return []; + } + + $throwType = $resolvedPhpDoc->getThrowsTag()->getType(); + + $errors = []; + foreach ($this->check->check($throwType, $statementResult->getThrowPoints()) as $throwClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s has %s in PHPDoc @throws tag but it\'s not thrown.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $throwClass, + )) + ->identifier('throws.unusedType') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideThrowTypeCheck.php b/src/Rules/Exceptions/TooWideThrowTypeCheck.php index 826b8daad7..1eb978a73f 100644 --- a/src/Rules/Exceptions/TooWideThrowTypeCheck.php +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -3,29 +3,38 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Analyser\ThrowPoint; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function array_map; -class TooWideThrowTypeCheck +#[AutowiredService] +final class TooWideThrowTypeCheck { + public function __construct( + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private bool $implicitThrows, + ) + { + } + /** - * @param Type $throwType * @param ThrowPoint[] $throwPoints * @return string[] */ public function check(Type $throwType, array $throwPoints): array { - if ($throwType instanceof VoidType) { + if ($throwType->isVoid()->yes()) { return []; } - $throwPointType = TypeCombinator::union(...array_map(static function (ThrowPoint $throwPoint): Type { - if (!$throwPoint->isExplicit()) { + $throwPointType = TypeCombinator::union(...array_map(function (ThrowPoint $throwPoint): Type { + if (!$this->implicitThrows && !$throwPoint->isExplicit()) { return new NeverType(); } diff --git a/src/Rules/FileRuleError.php b/src/Rules/FileRuleError.php index 7f5cd7fc26..612370f9b2 100644 --- a/src/Rules/FileRuleError.php +++ b/src/Rules/FileRuleError.php @@ -2,9 +2,12 @@ namespace PHPStan\Rules; +/** @api */ interface FileRuleError extends RuleError { public function getFile(): string; + public function getFileDescription(): string; + } diff --git a/src/Rules/FixableNodeRuleError.php b/src/Rules/FixableNodeRuleError.php new file mode 100644 index 0000000000..bdb5c973f1 --- /dev/null +++ b/src/Rules/FixableNodeRuleError.php @@ -0,0 +1,15 @@ + $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 @@ -46,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 f672293d66..57ae028018 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -2,75 +2,88 @@ namespace PHPStan\Rules; +use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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; - -class FunctionCallParametersCheck +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; + +#[AutowiredService] +final class FunctionCallParametersCheck { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private NullsafeCheck $nullsafeCheck; - - private PhpVersion $phpVersion; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkArgumentTypes; - - private bool $checkArgumentsPassedByReference; - - private bool $checkExtraArguments; - - private bool $checkMissingTypehints; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NullsafeCheck $nullsafeCheck, - PhpVersion $phpVersion, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkArgumentTypes, - bool $checkArgumentsPassedByReference, - bool $checkExtraArguments, - bool $checkMissingTypehints + private RuleLevelHelper $ruleLevelHelper, + private NullsafeCheck $nullsafeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PropertyReflectionFinder $propertyReflectionFinder, + #[AutowiredParameter(ref: '%checkFunctionArgumentTypes%')] + private bool $checkArgumentTypes, + #[AutowiredParameter] + private bool $checkArgumentsPassedByReference, + #[AutowiredParameter] + private bool $checkExtraArguments, + #[AutowiredParameter] + private bool $checkMissingTypehints, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nullsafeCheck = $nullsafeCheck; - $this->phpVersion = $phpVersion; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkArgumentTypes = $checkArgumentTypes; - $this->checkArgumentsPassedByReference = $checkArgumentsPassedByReference; - $this->checkExtraArguments = $checkExtraArguments; - $this->checkMissingTypehints = $checkMissingTypehints; } /** - * @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 array{string, string, string, string, string, string, string, string, string, string, string, string, string} $messages - * @return RuleError[] + * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @return list */ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, bool $isBuiltin, - $funcCall, - array $messages + Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, + string $nodeType, + TrinaryLogic $acceptsNamedArguments, + string $singleInsufficientParameterMessage, + string $pluralInsufficientParametersMessage, + string $singleInsufficientParameterInVariadicFunctionMessage, + string $pluralInsufficientParametersInVariadicFunctionMessage, + string $singleInsufficientParameterWithOptionalParametersMessage, + string $pluralInsufficientParametersWithOptionalParametersMessage, + string $wrongArgumentTypeMessage, + string $voidReturnTypeUsed, + string $parameterPassedByReferenceMessage, + string $unresolvableTemplateTypeMessage, + string $missingParameterMessage, + string $unknownParameterMessage, + string $unresolvableReturnTypeMessage, + string $unresolvableParameterTypeMessage, + string $namedArgumentMessage, ): array { $functionParametersMinCount = 0; @@ -87,46 +100,69 @@ public function check( $functionParametersMaxCount = -1; } - /** @var array $arguments */ + /** @var array $arguments */ $arguments = []; - /** @var array $args */ + /** @var array $args */ $args = $funcCall->getArgs(); $hasNamedArguments = false; $hasUnpackedArgument = false; $errors = []; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); + foreach ($args as $arg) { + $argumentName = null; + if ($arg->name !== null) { + $hasNamedArguments = true; + $argumentName = $arg->name->toString(); + } + if ($hasNamedArguments && $arg->unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.')->line($arg->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') + ->identifier('argument.unpackAfterNamed') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.')->line($arg->getLine())->nonIgnorable()->build(); + if ($argumentName === null || !$scope->getPhpVersion()->supportsNamedArgumentAfterUnpackedArgument()->yes()) { + $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') + ->identifier('argument.nonUnpackAfterUnpacked') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); + } } if ($arg->unpack) { $hasUnpackedArgument = true; } - $argumentName = null; - if ($arg->name !== null) { - $hasNamedArguments = true; - $argumentName = $arg->name->toString(); - } if ($arg->unpack) { - $arrays = TypeUtils::getConstantArrays($type); + $type = $scope->getType($arg->value); + $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { - $minKeys = null; + $maxKeys = null; foreach ($arrays as $array) { - $keysCount = count($array->getKeyTypes()); - if ($minKeys !== null && $keysCount >= $minKeys) { + $countType = $array->getArraySize(); + if ($countType instanceof ConstantIntegerType) { + $keysCount = $countType->getValue(); + } elseif ($countType instanceof IntegerRangeType) { + $keysCount = $countType->getMax(); + if ($keysCount === null) { + throw new ShouldNotHappenException(); + } + } else { + throw new ShouldNotHappenException(); + } + if ($maxKeys !== null && $keysCount >= $maxKeys) { continue; } - $minKeys = $keysCount; + $maxKeys = $keysCount; } - for ($j = 0; $j < $minKeys; $j++) { + for ($j = 0; $j < $maxKeys; $j++) { $types = []; $commonKey = null; + $isOptionalKey = false; foreach ($arrays as $constantArray) { + $isOptionalKey = in_array($j, $constantArray->getOptionalKeys(), true); $types[] = $constantArray->getValueTypes()[$j]; $keyType = $constantArray->getKeyTypes()[$j]; if ($commonKey === null) { @@ -140,12 +176,26 @@ public function check( $keyArgumentName = $commonKey; $hasNamedArguments = true; } + if ($isOptionalKey) { + continue; + } + $arguments[] = [ $arg->value, TypeCombinator::union(...$types), false, $keyArgumentName, - $arg->getLine(), + $arg->getStartLine(), + ]; + } + + if (count($arguments) === 0 && $type->isIterableAtLeastOnce()->yes()) { + $arguments[] = [ + $arg->value, + $type->getIterableValueType(), + true, + null, + $arg->getStartLine(), ]; } } else { @@ -154,7 +204,7 @@ public function check( $type->getIterableValueType(), true, null, - $arg->getLine(), + $arg->getStartLine(), ]; } continue; @@ -162,20 +212,24 @@ public function check( $arguments[] = [ $arg->value, - $type, + null, false, $argumentName, - $arg->getLine(), + $arg->getStartLine(), ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments()) { - $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.')->line($funcCall->getLine())->nonIgnorable()->build(); + if ($hasNamedArguments && !$scope->getPhpVersion()->supportsNamedArguments()->yes() && !(bool) $funcCall->getAttribute('isAttribute', false)) { + $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') + ->identifier('argument.namedNotSupported') + ->line($funcCall->getStartLine()) + ->nonIgnorable() + ->build(); } if (!$hasNamedArguments) { $invokedParametersCount = count($arguments); - foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + foreach ($arguments as [$argumentValue, $argumentValueType, $unpack, $argumentName]) { if ($unpack) { $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); break; @@ -188,36 +242,48 @@ public function check( ) { if ($functionParametersMinCount === $functionParametersMaxCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[0] : $messages[1], + $invokedParametersCount === 1 ? $singleInsufficientParameterMessage : $pluralInsufficientParametersMessage, $invokedParametersCount, - $functionParametersMinCount - ))->line($funcCall->getLine())->build(); + $functionParametersMinCount, + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[2] : $messages[3], + $invokedParametersCount === 1 ? $singleInsufficientParameterInVariadicFunctionMessage : $pluralInsufficientParametersInVariadicFunctionMessage, $invokedParametersCount, - $functionParametersMinCount - ))->line($funcCall->getLine())->build(); + $functionParametersMinCount, + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[4] : $messages[5], + $invokedParametersCount === 1 ? $singleInsufficientParameterWithOptionalParametersMessage : $pluralInsufficientParametersWithOptionalParametersMessage, $invokedParametersCount, $functionParametersMinCount, - $functionParametersMaxCount - ))->line($funcCall->getLine())->build(); + $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])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($voidReturnTypeUsed) + ->identifier(sprintf('%s.void', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } - [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getLine(), $isBuiltin, $arguments, $hasNamedArguments, $messages[10], $messages[11]); + [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $isBuiltin, $arguments, $hasNamedArguments, $missingParameterMessage, $unknownParameterMessage); foreach ($addedErrors as $error) { $errors[] = $error; } @@ -226,15 +292,13 @@ public function check( return $errors; } - foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter]) { + foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]) { if ($this->checkArgumentTypes && $unpack) { $iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $argumentValue, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $iterableTypeResultType = $iterableTypeResult->getType(); if ( @@ -244,8 +308,8 @@ 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 - ))->line($argumentLine)->build(); + $i + 1, + ))->identifier('argument.unpackNonIterable')->line($argumentLine)->build(); } } @@ -253,24 +317,86 @@ static function (Type $type): bool { continue; } - $parameterType = $parameter->getType(); - if ( - $this->checkArgumentTypes - && !$parameter->passedByReference()->createsNewVariable() - && !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()) - ) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); - $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription - ) : $parameterDescription, - $parameterType->describe($verbosityLevel), - $argumentValueType->describe($verbosityLevel) - ))->line($argumentLine)->build(); + if ($argumentValueType === null) { + if ($scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + $argumentValueType = $scope->getType($argumentValue); + + if ($scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + } + + if (!$acceptsNamedArguments->yes()) { + if ($argumentName !== null) { + $errors[] = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('named argument $%s', $argumentName))) + ->identifier('argument.named') + ->line($argumentLine) + ->build(); + } elseif ($unpack) { + $unpackedArrayType = $scope->getType($argumentValue); + $hasStringKey = $unpackedArrayType->getIterableKeyType()->isString(); + if (!$hasStringKey->no()) { + $errors[] = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('unpacked array with %s', $hasStringKey->yes() ? 'string key' : 'possibly string key'))) + ->identifier('argument.named') + ->line($argumentLine) + ->build(); + } + } + } + + if ($this->checkArgumentTypes) { + $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); + + if ( + !$parameter->passedByReference()->createsNewVariable() + || (!$isBuiltin && !$argumentValueType instanceof ErrorType) + ) { + $accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); + + if (!$accepts->result) { + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); + $errors[] = RuleErrorBuilder::message(sprintf( + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName ?? $i + 1), + $parameterType->describe($verbosityLevel), + $argumentValueType->describe($verbosityLevel), + )) + ->identifier('argument.type') + ->line($argumentLine) + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + } + + if ( + $originalParameter !== null + && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) + && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $unresolvableParameterTypeMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.unresolvableType')->line($argumentLine)->build(); + } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && ($argumentValue instanceof Expr\Closure || $argumentValue instanceof Expr\ArrowFunction) + && $argumentValue->static + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + 'bindable closure', + 'static closure', + )) + ->identifier('argument.staticClosure') + ->line($argumentLine) + ->build(); + } } if ( @@ -281,26 +407,63 @@ static function (Type $type): bool { } if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription - ))->line($argumentLine)->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + )) + ->identifier('argument.byRef') + ->line($argumentLine) + ->build(); continue; } - if ($argumentValue instanceof \PhpParser\Node\Expr\Variable - || $argumentValue instanceof \PhpParser\Node\Expr\ArrayDimFetch - || $argumentValue instanceof \PhpParser\Node\Expr\PropertyFetch - || $argumentValue instanceof \PhpParser\Node\Expr\StaticPropertyFetch) { + if ( + $argumentValue instanceof Node\Expr\PropertyFetch + || $argumentValue instanceof Node\Expr\StaticPropertyFetch) { + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($argumentValue, $scope); + foreach ($propertyReflections as $propertyReflection) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null) { + continue; + } + + if ($nativePropertyReflection->isReadOnly()) { + 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; } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription - ))->line($argumentLine)->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.byRef')->line($argumentLine)->build(); } if ($this->checkMissingTypehints && $parametersAcceptor instanceof ResolvedFunctionVariant) { @@ -308,19 +471,26 @@ static function (Type $type): bool { $resolvedTypes = $parametersAcceptor->getResolvedTemplateTypeMap()->getTypes(); if (count($resolvedTypes) > 0) { $returnTemplateTypes = []; - TypeTraverser::map($originalParametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type { - if ($type instanceof TemplateType) { - $returnTemplateTypes[$type->getName()] = true; - return $type; - } + TypeTraverser::map( + $parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(), + static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type { + while ($type instanceof ConditionalType && $type->isResolvable()) { + $type = $type->resolve(); + } - return $traverse($type); - }); + if ($type instanceof TemplateType && $type->getDefault() === null) { + $returnTemplateTypes[$type->getName()] = true; + return $type; + } + + return $traverse($type); + }, + ); $parameterTemplateTypes = []; foreach ($originalParametersAcceptor->getParameters() as $parameter) { TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type { - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $parameterTemplateTypes[$type->getName()] = true; return $type; } @@ -348,7 +518,11 @@ static function (Type $type): bool { continue; } - $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name))->line($funcCall->getLine())->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type')->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableTemplateTypeMessage, $name)) + ->identifier('argument.templateType') + ->line($funcCall->getStartLine()) + ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type') + ->build(); } } @@ -356,7 +530,10 @@ static function (Type $type): bool { !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) ) { - $errors[] = RuleErrorBuilder::message($messages[12])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } } @@ -364,12 +541,8 @@ static function (Type $type): bool { } /** - * @param ParametersAcceptor $parametersAcceptor - * @param array $arguments - * @param bool $hasNamedArguments - * @param string $missingParameterMessage - * @param string $unknownParameterMessage - * @return array{RuleError[], array} + * @param array $arguments + * @return array{list, array} */ private function processArguments( ParametersAcceptor $parametersAcceptor, @@ -378,15 +551,20 @@ private function processArguments( array $arguments, bool $hasNamedArguments, string $missingParameterMessage, - string $unknownParameterMessage + string $unknownParameterMessage, ): array { $parameters = $parametersAcceptor->getParameters(); + $originalParameters = $parametersAcceptor instanceof ResolvedFunctionVariant + ? $parametersAcceptor->getOriginalParametersAcceptor()->getParameters() + : array_fill(0, count($parameters), null); $parametersByName = []; + $originalParametersByName = []; $unusedParametersByName = []; $errors = []; - foreach ($parametersAcceptor->getParameters() as $parameter) { + foreach ($parameters as $i => $parameter) { $parametersByName[$parameter->getName()] = $parameter; + $originalParametersByName[$parameter->getName()] = $originalParameters[$i]; if ($parameter->isVariadic()) { continue; @@ -402,21 +580,24 @@ private function processArguments( if ($argumentName === null) { if (!isset($parameters[$i])) { if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) { - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; break; } $parameter = $parameters[count($parameters) - 1]; + $originalParameter = $originalParameters[count($originalParameters) - 1]; if (!$parameter->isVariadic()) { - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; break; // func_get_args } } else { $parameter = $parameters[$i]; + $originalParameter = $originalParameters[$i]; } } elseif (array_key_exists($argumentName, $parametersByName)) { $namedArgumentAlreadyOccurred = true; $parameter = $parametersByName[$argumentName]; + $originalParameter = $originalParametersByName[$argumentName]; } else { $namedArgumentAlreadyOccurred = true; @@ -426,28 +607,39 @@ private function processArguments( || $parametersCount <= 0 || $isBuiltin ) { - $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName))->line($argumentLine)->build(); - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName)) + ->identifier('argument.unknown') + ->line($argumentLine) + ->build(); + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } $parameter = $parameters[$parametersCount - 1]; + $originalParameter = $originalParameters[$parametersCount - 1]; } if ($namedArgumentAlreadyOccurred && $argumentName === null && !$unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.')->line($argumentLine)->nonIgnorable()->build(); - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.') + ->identifier('argument.positionalAfterNamed') + ->line($argumentLine) + ->nonIgnorable() + ->build(); + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]; if ( $hasNamedArguments && !$parameter->isVariadic() && !array_key_exists($parameter->getName(), $unusedParametersByName) ) { - $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName()))->line($argumentLine)->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName())) + ->identifier('argument.duplicate') + ->line($argumentLine) + ->build(); continue; } @@ -460,11 +652,33 @@ private function processArguments( continue; } - $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly()))))->line($line)->build(); + $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))) + ->identifier('argument.missing') + ->line($line) + ->build(); } } return [$errors, $newArguments]; } + private function describeParameter(ParameterReflection $parameter, int|string|null $positionOrNamed): string + { + $parts = []; + if (is_int($positionOrNamed)) { + $parts[] = 'Parameter #' . $positionOrNamed; + } elseif ($parameter->isVariadic() && is_string($positionOrNamed)) { + $parts[] = 'Named argument ' . $positionOrNamed . ' for variadic parameter'; + } else { + $parts[] = 'Parameter'; + } + + $name = $parameter->getName(); + if ($name !== '') { + $parts[] = ($parameter->isVariadic() ? '...$' : '$') . $name; + } + + return implode(' ', $parts); + } + } diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index db0d5b9c3a..6c749bca6b 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -2,108 +2,120 @@ 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\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Printer\NodeTypePrinter; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\NeverType; 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; + +#[AutowiredService] +final class FunctionDefinitionCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private PhpVersion $phpVersion; - - private bool $checkClassCaseSensitivity; - - private bool $checkThisOnly; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - PhpVersion $phpVersion, - bool $checkClassCaseSensitivity, - bool $checkThisOnly + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter] + private bool $checkThisOnly, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->phpVersion = $phpVersion; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; } /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage - * @return RuleError[] + * @return list */ public function checkFunction( + Scope $scope, Function_ $function, - FunctionReflection $functionReflection, + PhpFunctionFromParserNodeReflection $functionReflection, string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, + string $noDiscardVoidReturnMessage, ): array { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - return $this->checkParametersAcceptor( - $parametersAcceptor, + $scope, + $functionReflection, $function, $parameterMessage, $returnMessage, $unionTypesMessage, - $templateTypeMissingInParameterMessage + $templateTypeMissingInParameterMessage, + $unresolvableParameterTypeMessage, + $unresolvableReturnTypeMessage, + $noDiscardVoidReturnMessage, ); } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Param[] $parameters - * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\ComplexType|null $returnTypeNode - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @return \PHPStan\Rules\RuleError[] + * @param Node\Param[] $parameters + * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode + * @param Node\AttributeGroup[] $attribGroups + * @return list */ public function checkAnonymousFunction( Scope $scope, array $parameters, $returnTypeNode, + array $attribGroups, string $parameterMessage, string $returnMessage, - string $unionTypesMessage + string $unionTypesMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, + string $noDiscardReturnTypeMessage, ): array { $errors = []; $unionTypeReported = false; - foreach ($parameters as $param) { + foreach ($parameters as $i => $param) { if ($param->type === null) { continue; } @@ -112,28 +124,75 @@ public function checkAnonymousFunction( && $param->type instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($param->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($param->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; } if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \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), + ); } } @@ -144,79 +203,157 @@ public function checkAnonymousFunction( if ($returnTypeNode === null) { return $errors; } + if ( + $returnTypeNode instanceof Identifier + && in_array($returnTypeNode->toLowerString(), ['void', 'never'], true) + ) { + foreach ($attribGroups as $attribGroup) { + foreach ($attribGroup->attrs as $attrib) { + if (strtolower($attrib->name->name) === 'nodiscard') { + $errors[] = RuleErrorBuilder::message(sprintf($noDiscardReturnTypeMessage, $returnTypeNode->toString())) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->build(); + break 2; + } + } + } + } if ( !$unionTypeReported && $returnTypeNode instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); } $returnType = $scope->getFunctionType($returnTypeNode, false, false); + 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 - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage - * @return RuleError[] + * @return list */ public function checkClassMethod( + Scope $scope, PhpMethodFromParserNodeReflection $methodReflection, - ClassMethod $methodNode, + ClassMethod|Node\PropertyHook $methodNode, string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, + string $selfOutMessage, + string $noDiscardVoidReturnMessage, ): array { - /** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs $parametersAcceptor */ - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - - return $this->checkParametersAcceptor( - $parametersAcceptor, + $errors = $this->checkParametersAcceptor( + $scope, + $methodReflection, $methodNode, $parameterMessage, $returnMessage, $unionTypesMessage, - $templateTypeMissingInParameterMessage + $templateTypeMissingInParameterMessage, + $unresolvableParameterTypeMessage, + $unresolvableReturnTypeMessage, + $noDiscardVoidReturnMessage, ); + + $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 - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage - * @return RuleError[] + * @return list */ private function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + Scope $scope, + PhpMethodFromParserNodeReflection|PhpFunctionFromParserNodeReflection $parametersAcceptor, FunctionLike $functionNode, string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, + string $noDiscardReturnTypeMessage, ): array { $errors = []; @@ -228,14 +365,34 @@ private function checkParametersAcceptor( continue; } - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($parameterNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($parameterNode->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; break; } if (!$unionTypeReported && $functionNode->getReturnType() instanceof UnionType) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($functionNode->getReturnType()->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($functionNode->getReturnType()->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); + } + } + + foreach ($parameterNodes as $i => $parameterNode) { + if (!$parameterNode->var instanceof Variable || !is_string($parameterNode->var->name)) { + throw new ShouldNotHappenException(); } + $implicitlyNullableTypeError = $this->checkImplicitlyNullableType($parameterNode->type, $parameterNode->default, $i + 1, $parameterNode->getStartLine(), $parameterNode->var->name); + if ($implicitlyNullableTypeError === null) { + continue; + } + + $errors[] = $implicitlyNullableTypeError; } if ($this->phpVersion->deprecatesRequiredParameterAfterOptional()) { @@ -253,63 +410,151 @@ 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(), $scope->isInClass() ? $scope->getClassReflection() : null); + if ($this->unresolvableTypeHelper->containsUnresolvableType($nativeReturnType)) { + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->nonIgnorable() + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unresolvableNativeType') + ->build(); + } + } + if ($parametersAcceptor->mustUseReturnValue()->yes()) { + $returnType = $parametersAcceptor->getReturnType(); + if ( + $returnType->isVoid()->yes() + || ($returnType instanceof NeverType && $returnType->isExplicit()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($noDiscardReturnTypeMessage, $returnType->describe(VerbosityLevel::typeOnly()))) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->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(); @@ -326,8 +571,22 @@ private function checkParametersAcceptor( }); } + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof ConditionalTypeForParameter && !$returnType->isNegated()) { + TypeTraverser::map($returnType, static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + unset($templateTypes[$type->getName()]); + return $traverse($type); + } + + return $traverse($type); + }); + } + foreach (array_keys($templateTypes) as $templateTypeName) { - $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName)) + ->identifier('method.templateTypeNotInParameter') + ->build(); } } @@ -336,23 +595,34 @@ private function checkParametersAcceptor( /** * @param Param[] $parameterNodes - * @return RuleError[] + * @return list */ private function checkRequiredParameterAfterOptional(array $parameterNodes): array { /** @var string|null $optionalParameter */ $optionalParameter = null; $errors = []; + $targetPhpVersion = null; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_string($parameterNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameterName = $parameterNode->var->name; if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { - $errors[] = RuleErrorBuilder::message(sprintf('Deprecated in PHP 8.0: Required parameter $%s follows optional parameter $%s.', $parameterName, $optionalParameter))->line($parameterNode->getStartLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Deprecated in PHP %s: Required parameter $%s follows optional parameter $%s.', + $targetPhpVersion ?? '8.0', + $parameterName, + $optionalParameter, + ), + )->line($parameterNode->getStartLine()) + ->identifier('parameter.requiredAfterOptional') + ->build(); + $targetPhpVersion = null; continue; } if ($parameterNode->default === null) { @@ -371,7 +641,35 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr $constantName = $defaultValue->name->toLowerString(); if ($constantName === 'null') { - continue; + if (!$this->phpVersion->deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull()) { + continue; + } + + $parameterNodeType = $parameterNode->type; + + if ($parameterNodeType instanceof NullableType) { + $targetPhpVersion = '8.1'; + } + + if ($this->phpVersion->deprecatesRequiredParameterAfterOptionalUnionOrMixed()) { + $types = []; + + if ($parameterNodeType instanceof UnionType) { + $types = $parameterNodeType->types; + } elseif ($parameterNodeType instanceof Identifier) { + $types = [$parameterNodeType]; + } + + $nullOrMixed = array_filter($types, static fn (Identifier|Name|IntersectionType $type): bool => $type instanceof Identifier && (in_array($type->name, ['null', 'mixed'], true))); + + if (0 < count($nullOrMixed)) { + $targetPhpVersion = '8.3'; + } + } + + if ($targetPhpVersion === null) { + continue; + } } $optionalParameter = $parameterName; @@ -381,17 +679,15 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr } /** - * @param string $parameterName * @param Param[] $parameterNodes - * @return Param */ private function getParameterNode( string $parameterName, - array $parameterNodes + array $parameterNodes, ): Param { foreach ($parameterNodes as $param) { - if ($param->var instanceof \PhpParser\Node\Expr\Error) { + if ($param->var instanceof Node\Expr\Error) { continue; } @@ -404,16 +700,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(); } @@ -421,19 +716,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(); } @@ -443,8 +746,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 f3ee8d568b..e61965ca3a 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -2,35 +2,28 @@ namespace PHPStan\Rules; +use Generator; use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +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, @@ -41,33 +34,28 @@ public function checkReturnType( string $voidMessage, string $typeMismatchMessage, string $neverMessage, - bool $isGenerator + bool $isGenerator, ): array { + $returnType = TypeUtils::resolveLateResolvableTypes($returnType); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { return [ RuleErrorBuilder::message($neverMessage) - ->line($returnNode->getLine()) + ->line($returnNode->getStartLine()) + ->identifier('return.never') ->build(), ]; } if ($isGenerator) { - if (!$returnType instanceof TypeWithClassName) { - return []; - } - - $returnType = GenericTypeVariableResolver::getType( - $returnType, - \Generator::class, - 'TReturn' - ); - if ($returnType === null) { + $returnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if ($returnType instanceof ErrorType) { return []; } } - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, null); if ($returnValue === null) { if (!$isVoidSuperType->no()) { @@ -77,11 +65,18 @@ public function checkReturnType( return [ RuleErrorBuilder::message(sprintf( $emptyReturnStatementMessage, - $returnType->describe($verbosityLevel) - ))->line($returnNode->getLine())->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); @@ -89,18 +84,26 @@ public function checkReturnType( return [ RuleErrorBuilder::message(sprintf( $voidMessage, - $returnValueType->describe($verbosityLevel) - ))->line($returnNode->getLine())->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) - ))->line($returnNode->getLine())->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..f7c32cc2c7 --- /dev/null +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -0,0 +1,143 @@ + + */ +#[RegisteredRule(level: 5)] +final class ArrayFilterRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.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..0285a5c983 --- /dev/null +++ b/src/Rules/Functions/ArrayValuesRule.php @@ -0,0 +1,119 @@ + + */ +#[RegisteredRule(level: 5)] +final class ArrayValuesRule implements Rule +{ + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private readonly bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_values') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) === 0) { + return []; + } + + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } + + if ($arrayType->isIterableAtLeastOnce()->no()) { + $message = 'Parameter #1 $array (%s) to function array_values is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + if ($arrayType->isList()->yes()) { + $message = 'Parameter #1 $array (%s) of array_values is already a list, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.list'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isList()->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index b45c47f3c3..092758eaad 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class ArrowFunctionAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class ArrowFunctionAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string { - return Node\Expr\ArrowFunction::class; + return InArrowFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_FUNCTION, + 'function', ); } diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php index 0dac5917a3..676003ca17 100644 --- a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,14 +12,12 @@ /** * @implements Rule */ -class ArrowFunctionReturnNullsafeByRefRule implements Rule +#[RegisteredRule(level: 0)] +final class ArrowFunctionReturnNullsafeByRefRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string @@ -37,7 +36,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->nonIgnorable() + ->identifier('nullsafe.byRef') + ->build(), ]; } diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index 0a4cd265ad..ab1fecfe73 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -2,24 +2,26 @@ namespace PHPStan\Rules\Functions; +use Generator; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\VoidType; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InArrowFunctionNode> + * @implements Rule */ -class ArrowFunctionReturnTypeRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +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 @@ -30,19 +32,28 @@ 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 = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); if ($originalNode->returnType === null && $isVoidSuperType->yes()) { return []; } + $exprType = $scope->getType($originalNode->expr); + if ( + $returnType instanceof NeverType + && $returnType->isExplicit() + && $exprType instanceof NeverType + && $exprType->isExplicit() + ) { + return []; + } + return $this->returnTypeCheck->checkReturnType( $scope, $returnType, @@ -52,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function with return type void returns %s but should not return anything.', 'Anonymous function should return %s but returns %s.', 'Anonymous function should never return but return statement found.', - $generatorType->isSuperTypeOf($returnType)->yes() + $generatorType->isSuperTypeOf($returnType)->yes(), ); } diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index cbe6728ed7..4ab0af4b01 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -2,62 +2,63 @@ namespace PHPStan\Rules\Functions; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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, + #[AutowiredParameter] + 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) { @@ -68,21 +69,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 @@ -92,14 +98,15 @@ 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->getArgs(), - $parametersAcceptors + $parametersAcceptors, + null, ); if ($type instanceof ClosureType) { @@ -115,22 +122,24 @@ static function (Type $type): bool { $scope, false, $node, - [ - ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', - 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to ' . $callableDescription, - 'Missing parameter $%s in call to ' . $callableDescription . '.', - 'Unknown parameter $%s in call to ' . $callableDescription . '.', - 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - ] - ) + 'callable', + $acceptsNamedArguments, + ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $callableDescription . ' expects %s, %s given.', + 'Result of ' . $callableDescription . ' (void) is used.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $callableDescription, + 'Missing parameter $%s in call to ' . $callableDescription . '.', + 'Unknown parameter $%s in call to ' . $callableDescription . '.', + 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 81371eadb7..39f6f7cfea 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -5,25 +5,22 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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 @@ -33,7 +30,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 []; } @@ -48,26 +45,29 @@ public function processNode(Node $node, Scope $scope): array ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), - $function->getVariants() + $function->getVariants(), + $function->getNamedArgumentsVariants(), ), $scope, $function->isBuiltin(), $node, - [ - 'Function ' . $functionName . ' invoked with %d parameter, %d required.', - 'Function ' . $functionName . ' invoked with %d parameters, %d required.', - 'Function ' . $functionName . ' invoked with %d parameter, at least %d required.', - 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', - 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', - 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of function ' . $functionName . ' expects %s, %s given.', - 'Result of function ' . $functionName . ' (void) is used.', - 'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to function ' . $functionName, - 'Missing parameter $%s in call to function ' . $functionName . '.', - 'Unknown parameter $%s in call to function ' . $functionName . '.', - 'Return type of call to function ' . $functionName . ' contains unresolvable type.', - ] + 'function', + $function->acceptsNamedArguments(), + 'Function ' . $functionName . ' invoked with %d parameter, %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', + '%s of function ' . $functionName . ' expects %s, %s given.', + 'Result of function ' . $functionName . ' (void) is used.', + '%s of function ' . $functionName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to function ' . $functionName, + 'Missing parameter $%s in call to function ' . $functionName . '.', + 'Unknown parameter $%s in call to function ' . $functionName . '.', + 'Return type of call to function ' . $functionName . ' contains unresolvable type.', + '%s of function ' . $functionName . ' contains unresolvable type.', + 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); } diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..7ac71862e3 --- /dev/null +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -0,0 +1,82 @@ + + */ +#[RegisteredRule(level: 0)] +final class CallToFunctionStatementWithNoDiscardRule implements Rule +{ + + 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 []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + if ($funcCall->name instanceof Node\Name) { + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); + if (!$function->mustUseReturnValue()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line discards return value.', + $function->getName(), + ))->identifier('function.resultDiscarded')->build(), + ]; + } + + $callableType = $scope->getType($funcCall->name); + if (!$callableType->isCallable()->yes()) { + return []; + } + + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($callableType->getCallableParametersAcceptors($scope) as $callableParametersAcceptor) { + $mustUseReturnValue = $mustUseReturnValue->or($callableParametersAcceptor->mustUseReturnValue()); + } + + if (!$mustUseReturnValue->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to callable %s on a separate line discards return value.', + $callableType->describe(VerbosityLevel::value()), + ))->identifier('callable.resultDiscarded')->build(), + ]; + } + +} diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 5c4bc21420..a117c228fd 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -3,24 +3,46 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; +use PhpParser\Node\Arg; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; +use PHPStan\Type\Type; +use function count; +use function in_array; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToFunctionStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToFunctionStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; + 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 function __construct(ReflectionProvider $reflectionProvider) + ]; + + public const PHPSTAN_TESTING_FUNCTIONS = [ + 'PHPStan\\dumpNativeType', + 'PHPStan\\dumpType', + 'PHPStan\\dumpPhpDocType', + 'PHPStan\\debugScope', + 'PHPStan\\Testing\\assertType', + 'PHPStan\\Testing\\assertNativeType', + 'PHPStan\\Testing\\assertSuperType', + 'PHPStan\\Testing\\assertVariableCertainty', + ]; + + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array } $funcCall = $node->expr; - if (!($funcCall->name instanceof \PhpParser\Node\Name)) { + if (!($funcCall->name instanceof Node\Name)) { return []; } @@ -44,30 +66,80 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if ($function->hasSideEffects()->no()) { - $throwsType = $function->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; + if (count($function->getAsserts()->getAsserts()) > 0) { + return []; + } + + $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; + } } - $functionResult = $scope->getType($funcCall); - if ($functionResult instanceof NeverType && $functionResult->isExplicit()) { + if (!$sideEffectFlipped) { return []; } - if (in_array($function->getName(), [ - 'PHPStan\\Testing\\assertType', - 'PHPStan\\Testing\\assertNativeType', - 'PHPStan\\Testing\\assertVariableCertainty', - ], true)) { + $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() - ))->build(), + $function->getName(), + ))->identifier('function.resultUnused')->build(), ]; } diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index b4a10083f2..88f98f4927 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -5,26 +5,29 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +final class CallToNonExistentFunctionRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string @@ -34,13 +37,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))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; } @@ -58,8 +72,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..3dae092b52 --- /dev/null +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -0,0 +1,90 @@ + + */ +#[RegisteredRule(level: 5)] +final class CallUserFuncRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'call_user_func') { + return []; + } + + $result = ArgumentsNormalizer::reorderCallUserFuncArguments( + $node, + $scope, + ); + if ($result === null) { + return []; + } + [$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result; + + $callableDescription = 'callable passed to call_user_func()'; + + return $this->check->check( + $parametersAcceptor, + $scope, + false, + $funcCall, + 'function', + $acceptsNamedArguments, + ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $callableDescription . ' expects %s, %s given.', + 'Result of ' . $callableDescription . ' (void) is used.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $callableDescription, + 'Missing parameter $%s in call to ' . $callableDescription . '.', + 'Unknown parameter $%s in call to ' . $callableDescription . '.', + 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + ); + } + +} diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 2048c09a8b..d9dd348f9c 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InClosureNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class ClosureAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class ClosureAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string { - return Node\Expr\Closure::class; + return InClosureNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_FUNCTION, + 'function', ); } diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index d1dac5ad77..abeb7c3866 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -4,21 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Type\TypeCombinator; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ClosureReturnTypeRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +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 @@ -32,7 +32,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PHPStan\Type\Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $containsNull = TypeCombinator::containsNull($returnType); $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; @@ -53,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function with return type void returns %s but should not return anything.', 'Anonymous function should return %s but returns %s.', 'Anonymous function should never return but return statement found.', - count($node->getYieldStatements()) > 0 + $node->isGenerator(), ); foreach ($returnMessages as $returnMessage) { diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php new file mode 100644 index 0000000000..3534ebc70b --- /dev/null +++ b/src/Rules/Functions/DefineParametersRule.php @@ -0,0 +1,59 @@ + + */ +#[RegisteredRule(level: 0)] +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 85de1d6b5c..827784dd9a 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,19 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +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,14 +31,29 @@ 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(), + $node->getAttrGroups(), '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.' - ); + '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.', + 'Attribute NoDiscard cannot be used on %s anonymous function.', + )); } } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index 9033078f9b..24c91a88c1 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -5,19 +5,19 @@ use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Closure> + * @implements Rule */ -class ExistingClassesInClosureTypehintsRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +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,9 +31,13 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), + $node->getAttrGroups(), '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.' + '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.', + 'Attribute NoDiscard cannot be used on %s anonymous function.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 04256e7074..66303ad26b 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -4,22 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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 @@ -29,25 +29,34 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->getFunction() instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName()); + $functionName = SprintfHelper::escapeFormatString($node->getFunctionReflection()->getName()); return $this->check->checkFunction( + $scope, $node->getOriginalNode(), - $scope->getFunction(), + $node->getFunctionReflection(), sprintf( 'Parameter $%%s of function %s() has invalid type %%s.', - $functionName + $functionName, ), sprintf( 'Function %s() has invalid return type %%s.', - $functionName + $functionName, ), sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName), - sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName) + sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName), + sprintf( + 'Parameter $%%s of function %s() has unresolvable native type.', + $functionName, + ), + sprintf( + 'Function %s() has unresolvable native return type.', + $functionName, + ), + sprintf( + 'Attribute NoDiscard cannot be used on %%s function %s().', + $functionName, + ), ); } diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 758d779b47..a7b6547cb0 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class FunctionAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class FunctionAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string { - return Node\Stmt\Function_::class; + return InFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + $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..a4b698d357 --- /dev/null +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -0,0 +1,124 @@ + + */ +#[RegisteredRule(level: 0)] +final class FunctionCallableRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $checkFunctionNameCase, + #[AutowiredParameter] + 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/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php deleted file mode 100644 index 0bd596d75a..0000000000 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -class ImplodeFunctionRule implements \PHPStan\Rules\Rule -{ - - private RuleLevelHelper $ruleLevelHelper; - - private ReflectionProvider $reflectionProvider; - - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper - ) - { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - } - - public function getNodeType(): string - { - return FuncCall::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!($node->name instanceof \PhpParser\Node\Name)) { - return []; - } - - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if (!in_array($functionName, ['implode', 'join'], true)) { - return []; - } - - $args = $node->getArgs(); - if (count($args) === 1) { - $arrayArg = $args[0]->value; - $paramNo = 1; - } elseif (count($args) === 2) { - $arrayArg = $args[1]->value; - $paramNo = 2; - } else { - return []; - } - - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $arrayArg, - '', - static function (Type $type): bool { - return !$type->getIterableValueType()->toString() instanceof ErrorType; - } - ); - - if ($typeResult->getType() instanceof ErrorType - || !$typeResult->getType()->getIterableValueType()->toString() instanceof ErrorType) { - return []; - } - - return [ - RuleErrorBuilder::message( - sprintf('Parameter #%d $array of function %s expects array, %s given.', $paramNo, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())) - )->build(), - ]; - } - -} diff --git a/src/Rules/Functions/ImplodeParameterCastableToStringRule.php b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php new file mode 100644 index 0000000000..f58a4ca2be --- /dev/null +++ b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php @@ -0,0 +1,119 @@ + + */ +#[RegisteredRule(level: 5)] +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..ec31655fc5 --- /dev/null +++ b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php @@ -0,0 +1,72 @@ + + */ +#[RegisteredRule(level: 2)] +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..9562e5086b --- /dev/null +++ b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php @@ -0,0 +1,72 @@ + + */ +#[RegisteredRule(level: 2)] +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 031a4b9c31..6deb4c3f45 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -4,18 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -25,12 +28,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,14 +38,15 @@ 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; } @@ -59,8 +58,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..b763b492e5 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -5,12 +5,15 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> + * @implements Rule */ -class InnerFunctionRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +final class InnerFunctionRule implements Rule { public function getNodeType(): string @@ -26,8 +29,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..eab6958f68 --- /dev/null +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -0,0 +1,88 @@ + + */ +#[RegisteredRule(level: 0)] +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 5a86ac7662..908430a5bb 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -4,29 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 6)] +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 +36,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 +63,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 type specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName() - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -75,30 +81,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(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->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 parameter $%s with no signature specified for %s.', + 'Function %s() has %s with no signature specified for %s.', $functionReflection->getName(), - $parameterReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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 57d6533169..761a8fed31 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -4,27 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 6)] +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 +33,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $functionReflection = $node->getFunctionReflection(); + $returnType = $functionReflection->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( 'Function %s() has no return type specified.', - $functionReflection->getName() - ))->build(), + $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(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $messages[] = RuleErrorBuilder::message(sprintf('Function %s() return type has no value type specified in iterable type %s.', $functionReflection->getName(), $iterableTypeDescription)) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -61,16 +59,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()) - ))->build(); + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index bb4aada71d..ad67abb22b 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -2,22 +2,22 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** * @implements Rule */ -class ParamAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class ParamAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -28,26 +28,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $targetName = 'parameter'; + $targetType = Attribute::TARGET_PARAMETER; if ($node->flags !== 0) { $targetName = 'parameter or property'; - - $propertyTargetErrors = $this->attributesCheck->check( - $scope, - $node->attrGroups, - \Attribute::TARGET_PROPERTY, - $targetName - ); - - if (count($propertyTargetErrors) === 0) { - return $propertyTargetErrors; - } + $targetType |= Attribute::TARGET_PROPERTY; } return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_PARAMETER, - $targetName + $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..bbc6e0be36 --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -0,0 +1,120 @@ + + */ +#[RegisteredRule(level: 5)] +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..1d7e093116 --- /dev/null +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -0,0 +1,190 @@ + + */ +#[RegisteredRule(level: 0)] +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..73cb4af220 --- /dev/null +++ b/src/Rules/Functions/PrintfHelper.php @@ -0,0 +1,137 @@ +[bs%s]|l?[cdeEgfFGouxX])'; + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getPrintfPlaceholdersCount(string $format): int + { + return $this->getPlaceholdersCount(self::PRINTF_SPECIFIER_PATTERN, $format); + } + + /** @phpstan-return array> parameter index => placeholders */ + public function getPrintfPlaceholders(string $format): array + { + return $this->parsePlaceholders(self::PRINTF_SPECIFIER_PATTERN, $format); + } + + public function getScanfPlaceholdersCount(string $format): int + { + return $this->getPlaceholdersCount('(?[cdDeEfinosuxX%s]|\[[^\]]+\])', $format); + } + + /** @phpstan-return array> parameter index => placeholders */ + private function parsePlaceholders(string $specifiersPattern, string $format): array + { + $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 []; + } + + $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); + + $result = []; + $parsedPlaceholders = []; + $parameterIdx = 0; + $placeholderNumber = 0; + + foreach ($placeholders as $placeholder) { + $placeholderNumber++; + $showValueSuffix = false; + + if (isset($placeholder['width']) && $placeholder['width'] !== '') { + $parsedPlaceholders[] = new PrintfPlaceholder( + sprintf('"%s" (width)', $placeholder[0]), + $parameterIdx++, + $placeholderNumber, + 'strict-int', + ); + $showValueSuffix = true; + } + + if (isset($placeholder['precision']) && $placeholder['precision'] !== '') { + $parsedPlaceholders[] = new PrintfPlaceholder( + sprintf('"%s" (precision)', $placeholder[0]), + $parameterIdx++, + $placeholderNumber, + 'strict-int', + ); + $showValueSuffix = true; + } + + $parsedPlaceholders[] = new PrintfPlaceholder( + sprintf('"%s"', $placeholder[0]) . ($showValueSuffix ? ' (value)' : ''), + isset($placeholder['position']) && $placeholder['position'] !== '' + ? $placeholder['position'] - 1 + : $parameterIdx++, + $placeholderNumber, + $this->getAcceptingTypeBySpecifier($placeholder['specifier'] ?? ''), + ); + } + + foreach ($parsedPlaceholders as $placeholder) { + $result[$placeholder->parameterIndex][] = $placeholder; + } + + return $result; + } + + /** @phpstan-return 'string'|'int'|'float'|'mixed' */ + private function getAcceptingTypeBySpecifier(string $specifier): string + { + if ($specifier === 's') { + return 'string'; + } + + if (in_array($specifier, ['d', 'u', 'c', 'o', 'x', 'X', 'b'], true)) { + return 'int'; + } + + if (in_array($specifier, ['e', 'E', 'f', 'F', 'g', 'G', 'h', 'H'], true)) { + return 'float'; + } + + return 'mixed'; + } + + private function getPlaceholdersCount(string $specifiersPattern, string $format): int + { + $paramIndices = array_keys($this->parsePlaceholders($specifiersPattern, $format)); + + return $paramIndices === [] + ? 0 + // The indices start from 0 + : max($paramIndices) + 1; + } + +} diff --git a/src/Rules/Functions/PrintfParameterTypeRule.php b/src/Rules/Functions/PrintfParameterTypeRule.php new file mode 100644 index 0000000000..d91da4590e --- /dev/null +++ b/src/Rules/Functions/PrintfParameterTypeRule.php @@ -0,0 +1,164 @@ + + */ +final class PrintfParameterTypeRule implements Rule +{ + + private const FORMAT_ARGUMENT_POSITIONS = [ + 'printf' => 0, + 'sprintf' => 0, + 'fprintf' => 1, + ]; + private const MINIMUM_NUMBER_OF_ARGUMENTS = [ + 'printf' => 1, + 'sprintf' => 1, + 'fprintf' => 2, + ]; + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private bool $checkStrictPrintfPlaceholderTypes, + ) + { + } + + 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); + $name = $functionReflection->getName(); + if (!array_key_exists($name, self::FORMAT_ARGUMENT_POSITIONS)) { + return []; + } + + $formatArgumentPosition = self::FORMAT_ARGUMENT_POSITIONS[$name]; + + $args = $node->getArgs(); + foreach ($args as $arg) { + if ($arg->unpack) { + return []; + } + } + $argsCount = count($args); + if ($argsCount < self::MINIMUM_NUMBER_OF_ARGUMENTS[$name]) { + return []; // caught by CallToFunctionParametersRule + } + + $formatArgType = $scope->getType($args[$formatArgumentPosition]->value); + $formatArgTypeStrings = $formatArgType->getConstantStrings(); + + // Let's start simple for now. + if (count($formatArgTypeStrings) !== 1) { + return []; + } + + $formatString = $formatArgTypeStrings[0]; + $format = $formatString->getValue(); + $placeholderMap = $this->printfHelper->getPrintfPlaceholders($format); + $errors = []; + $typeAllowedByCallToFunctionParametersRule = TypeCombinator::union( + new StringAlwaysAcceptingObjectWithToStringType(), + new IntegerType(), + new FloatType(), + new BooleanType(), + new NullType(), + ); + // Type on the left can go to the type on the right, but not vice versa. + $allowedTypeNameMap = $this->checkStrictPrintfPlaceholderTypes + ? [ + 'strict-int' => 'int', + 'int' => 'int', + 'float' => 'float', + 'string' => 'string', + 'mixed' => 'string', + ] + : [ + 'strict-int' => 'int', + 'int' => 'castable to int', + 'float' => 'castable to float', + // These are here just for completeness. They won't be used because, these types are already enforced by + // CallToFunctionParametersRule. + 'string' => 'castable to string', + 'mixed' => 'castable to string', + ]; + + for ($i = $formatArgumentPosition + 1, $j = 0; $i < $argsCount; $i++, $j++) { + // Some arguments may be skipped entirely. + foreach ($placeholderMap[$j] ?? [] as $placeholder) { + $argType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $args[$i]->value, + '', + fn (Type $t) => $placeholder->doesArgumentTypeMatchPlaceholder($t, $this->checkStrictPrintfPlaceholderTypes), + )->getType(); + + if ($argType instanceof ErrorType || $placeholder->doesArgumentTypeMatchPlaceholder($argType, $this->checkStrictPrintfPlaceholderTypes)) { + continue; + } + + // This is already reported by CallToFunctionParametersRule + if ( + !$this->ruleLevelHelper->accepts( + $typeAllowedByCallToFunctionParametersRule, + $argType, + $scope->isDeclareStrictTypes(), + )->result + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Parameter #%d of function %s is expected to be %s by placeholder #%d (%s), %s given.', + $i + 1, + $name, + $allowedTypeNameMap[$placeholder->acceptingType], + $placeholder->placeholderNumber, + $placeholder->label, + $argType->describe(VerbosityLevel::typeOnly()), + ), + )->identifier('argument.type')->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index 96d38ed36c..44b02b14e9 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -5,21 +5,40 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +final class PrintfParametersRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + private const FORMAT_ARGUMENT_POSITIONS = [ + 'printf' => 0, + 'sprintf' => 0, + 'sscanf' => 1, + 'fscanf' => 1, + ]; + private const MINIMUM_NUMBER_OF_ARGUMENTS = [ + 'printf' => 1, + 'sprintf' => 1, + 'sscanf' => 3, + 'fscanf' => 3, + ]; + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + ) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -29,29 +48,21 @@ 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->getArgs(); foreach ($args as $arg) { @@ -60,83 +71,50 @@ public function processNode(Node $node, Scope $scope): array } } $argsCount = count($args); - if ($argsCount < $minimumNumberOfArguments[$name]) { + if ($argsCount < self::MINIMUM_NUMBER_OF_ARGUMENTS[$name]) { return []; // caught by CallToFunctionParametersRule } $formatArgType = $scope->getType($args[$formatArgumentPosition]->value); - $placeHoldersCount = null; - foreach (TypeUtils::getConstantStrings($formatArgType) as $formatString) { + $maxPlaceHoldersCount = null; + foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); - $tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format); - if ($placeHoldersCount === null) { - $placeHoldersCount = $tempPlaceHoldersCount; - } elseif ($tempPlaceHoldersCount > $placeHoldersCount) { - $placeHoldersCount = $tempPlaceHoldersCount; + + if (in_array($name, ['sprintf', 'printf'], true)) { + $tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format); + } else { + $tempPlaceHoldersCount = $this->printfHelper->getScanfPlaceholdersCount($format); + } + + if ($maxPlaceHoldersCount === null) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; + } elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; } } - if ($placeHoldersCount === null) { + if ($maxPlaceHoldersCount === null) { return []; } $argsCount -= $formatArgumentPosition; - if ($argsCount !== $placeHoldersCount + 1) { + if ($argsCount !== $maxPlaceHoldersCount + 1) { return [ RuleErrorBuilder::message(sprintf( sprintf( '%s, %s.', - $placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', - $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%s]' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])'; - $addSpecifier = ''; - if ($this->phpVersion->supportsHhPrintfSpecifier()) { - $addSpecifier .= 'hH'; - } - - $specifiers = sprintf($specifiers, $addSpecifier); - - $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/PrintfPlaceholder.php b/src/Rules/Functions/PrintfPlaceholder.php new file mode 100644 index 0000000000..4bdeb156d6 --- /dev/null +++ b/src/Rules/Functions/PrintfPlaceholder.php @@ -0,0 +1,57 @@ +acceptingType) { + case 'strict-int': + return (new IntegerType())->accepts($argumentType, true)->yes(); + case 'int': + return $strictPlaceholderTypes + ? (new IntegerType())->accepts($argumentType, true)->yes() + : ! $argumentType->toInteger() instanceof ErrorType; + case 'float': + return $strictPlaceholderTypes + ? (new FloatType())->accepts($argumentType, true)->yes() + : ! $argumentType->toFloat() instanceof ErrorType; + case 'string': + case 'mixed': + // The function signature already limits the parameters to stringable types, so there's + // no point in checking string again here. + return !$strictPlaceholderTypes + // Don't accept null or bool. It's likely to be a mistake. + || TypeCombinator::union( + new StringAlwaysAcceptingObjectWithToStringType(), + // float also accepts int. + new FloatType(), + )->accepts($argumentType, true)->yes(); + // Without this PHPStan with PHP 7.4 reports "...should return bool but return statement is missing." + // Presumably, because promoted properties are turned into regular properties and the phpdoc isn't applied to the property. + default: + throw new ShouldNotHappenException('Unexpected type ' . $this->acceptingType); + } + } + +} diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php index 576116cd93..d2f8c52597 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -5,26 +5,33 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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\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 +#[RegisteredRule(level: 5)] +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, + #[AutowiredParameter] + private bool $reportMaybes, + ) { - $this->reflectionProvider = $reflectionProvider; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -34,7 +41,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -42,8 +49,13 @@ public function processNode(Node $node, Scope $scope): array return []; } - $minType = $scope->getType($node->getArgs()[0]->value)->toInteger(); - $maxType = $scope->getType($node->getArgs()[1]->value)->toInteger(); + $args = array_values($node->getArgs()); + if (count($args) < 2) { + return []; + } + + $minType = $scope->getType($args[0]->value)->toInteger(); + $maxType = $scope->getType($args[1]->value)->toInteger(); if ( !$minType instanceof ConstantIntegerType && !$minType instanceof IntegerRangeType @@ -52,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isSmaller = $maxType->isSmallerThan($minType); + $isSmaller = $maxType->isSmallerThan($minType, $this->phpVersion); if ($isSmaller->yes() || $isSmaller->maybe() && $this->reportMaybes) { $message = 'Parameter #1 $min (%s) of function random_int expects lower number than parameter #2 $max (%s).'; @@ -60,8 +72,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $message, $minType->describe(VerbosityLevel::value()), - $maxType->describe(VerbosityLevel::value()) - ))->build(), + $maxType->describe(VerbosityLevel::value()), + ))->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Functions/RedefinedParametersRule.php b/src/Rules/Functions/RedefinedParametersRule.php new file mode 100644 index 0000000000..e4c37614f4 --- /dev/null +++ b/src/Rules/Functions/RedefinedParametersRule.php @@ -0,0 +1,62 @@ + + */ +#[RegisteredRule(level: 0)] +final class RedefinedParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $params = $node->getParams(); + + if (count($params) <= 1) { + return []; + } + + $vars = []; + $errors = []; + + foreach ($params as $param) { + if (!$param->var instanceof Node\Expr\Variable) { + continue; + } + + if (!is_string($param->var->name)) { + continue; + } + + $var = $param->var->name; + + if (!isset($vars[$var])) { + $vars[$var] = true; + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Redefinition of parameter $%s.', $var)) + ->identifier('parameter.duplicate') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/ReturnNullsafeByRefRule.php b/src/Rules/Functions/ReturnNullsafeByRefRule.php index a232103ca1..44121d713b 100644 --- a/src/Rules/Functions/ReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ReturnNullsafeByRefRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ReturnStatementsNode; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\Rule; @@ -12,14 +13,12 @@ /** * @implements Rule */ -class ReturnNullsafeByRefRule implements Rule +#[RegisteredRule(level: 0)] +final class ReturnNullsafeByRefRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string @@ -44,7 +43,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->line($returnNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->line($returnNode->getStartLine()) + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Functions/ReturnTypeRule.php b/src/Rules/Functions/ReturnTypeRule.php index a554fcba21..157f03f2b2 100644 --- a/src/Rules/Functions/ReturnTypeRule.php +++ b/src/Rules/Functions/ReturnTypeRule.php @@ -5,30 +5,23 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ReturnTypeRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +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,46 +40,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() + $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..1625024bd6 --- /dev/null +++ b/src/Rules/Functions/SortParameterCastableToStringRule.php @@ -0,0 +1,152 @@ + + */ +#[RegisteredRule(level: 5)] +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 d26dac08a1..8a4bb345b6 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -4,19 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 1)] +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,21 +34,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', - [ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ] + 'closure.unusedUse', ); } diff --git a/src/Rules/Functions/UselessFunctionReturnValueRule.php b/src/Rules/Functions/UselessFunctionReturnValueRule.php new file mode 100644 index 0000000000..92ef7d4e57 --- /dev/null +++ b/src/Rules/Functions/UselessFunctionReturnValueRule.php @@ -0,0 +1,91 @@ + + */ +#[RegisteredRule(level: 4)] +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..73ca669ff1 --- /dev/null +++ b/src/Rules/Functions/VariadicParametersDeclarationRule.php @@ -0,0 +1,53 @@ + + */ +#[RegisteredRule(level: 0)] +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 8e6980eae2..b77f04b53a 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -2,36 +2,33 @@ namespace PHPStan\Rules\Generators; +use Generator; use PhpParser\Node; use PhpParser\Node\Expr\YieldFrom; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\YieldFrom> + * @implements Rule */ -class YieldFromTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldFromTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -48,8 +45,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 @@ -59,8 +59,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(), ]; } @@ -69,7 +72,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 } @@ -79,21 +82,32 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $exprType->getIterableKeyType()); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), - $exprType->getIterableKeyType()->describe($verbosityLevel) - ))->line($node->expr->getLine())->build(); + $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())) { + + $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(); @@ -101,18 +115,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; } @@ -121,18 +127,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->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 559dc7f090..601c8f43a4 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -4,24 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class YieldInGeneratorRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldInGeneratorRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct( + #[AutowiredParameter] + private bool $reportMaybes, + ) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -40,9 +43,14 @@ public function processNode(Node $node, Scope $scope): array if ($anonymousFunctionReturnType !== null) { $returnType = $anonymousFunctionReturnType; } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); + $returnType = $scopeFunction->getReturnType(); } else { - return [RuleErrorBuilder::message('Yield can be used only inside a function.')->build()]; + return [ + RuleErrorBuilder::message('Yield can be used only inside a function.') + ->identifier('generator.outOfFunction') + ->nonIgnorable() + ->build(), + ]; } if ($returnType instanceof MixedType) { @@ -53,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array $isSuperType = TrinaryLogic::createNo(); } else { $isSuperType = $returnType->isIterable()->and(TrinaryLogic::createFromBoolean( - !$returnType->isArray()->yes() + !$returnType->isArray()->yes(), )); } if ($isSuperType->yes()) { @@ -67,8 +75,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 f0b04ba578..e5f94c8193 100644 --- a/src/Rules/Generators/YieldTypeRule.php +++ b/src/Rules/Generators/YieldTypeRule.php @@ -4,7 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -12,21 +12,19 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Yield_> + * @implements Rule */ -class YieldTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -41,7 +39,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 } @@ -56,31 +54,42 @@ public function processNode(Node $node, Scope $scope): array $keyType = $scope->getType($node->key); } - if ($node->value === null) { - $valueType = new NullType(); - } else { - $valueType = $scope->getType($node->value); - } - $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $keyType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), - $keyType->describe($verbosityLevel) - ))->build(); + $keyType->describe($verbosityLevel), + )) + ->acceptsReasonsTip($acceptsKey->reasons) + ->identifier('generator.keyType') + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) { + + if ($node->value === null) { + $valueType = new NullType(); + } else { + $valueType = $scope->getType($node->value); + } + + $acceptsValue = $this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes()); + if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $valueType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), - $valueType->describe($verbosityLevel) - ))->build(); + $valueType->describe($verbosityLevel), + )) + ->acceptsReasonsTip($acceptsValue->reasons) + ->identifier('generator.valueType') + ->build(); } - if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index 96c64dfef7..9066de4f4b 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -4,35 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 + * @implements Rule */ -class ClassAncestorsRule implements Rule +#[RegisteredRule(level: 2)] +final class ClassAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - - private CrossCheckInterfacesHelper $crossCheckInterfacesHelper; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck, - CrossCheckInterfacesHelper $crossCheckInterfacesHelper + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; - $this->crossCheckInterfacesHelper = $crossCheckInterfacesHelper; } public function getNodeType(): string @@ -46,64 +40,45 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if ($classReflection->isAnonymous()) { return []; } $className = $classReflection->getName(); - - $extendsTags = []; - $implementsTags = []; - $docComment = $originalNode->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); - } - $escapedClassName = SprintfHelper::escapeFormatString($className); $extendsErrors = $this->genericAncestorsCheck->check( $originalNode->extends !== null ? [$originalNode->extends] : [], - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), + 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 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.', + 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', + 'Generic type %s in PHPDoc tag @extends does not specify all template types of %s %s: %s', + 'Generic type %s in PHPDoc tag @extends specifies %d template types, but %s %s supports only %d: %s', + 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), - sprintf('in extended type %%s of class %s', $escapedClassName) + sprintf('in extended type %%s of class %s', $escapedClassName), ); $implementsErrors = $this->genericAncestorsCheck->check( $originalNode->implements, - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), + 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 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.', + 'PHPDoc tag @implements contains generic type %s but %s %s is not generic.', + 'Generic type %s in PHPDoc tag @implements does not specify all template types of %s %s: %s', + 'Generic type %s in PHPDoc tag @implements specifies %d template types, but %s %s supports only %d: %s', + 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @implements is not allowed.', 'PHPDoc tag @implements has invalid type %s.', sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), - sprintf('in implemented type %%s of class %s', $escapedClassName) + sprintf('in implemented type %%s of class %s', $escapedClassName), ); foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index f611c41093..b3515e927d 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -4,24 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ClassTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class ClassTemplateTypeRule implements Rule { - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - TemplateTypeCheck $templateTypeCheck + private TemplateTypeCheck $templateTypeCheck, ) { - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -31,10 +31,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isClass()) { return []; } - $classReflection = $scope->getClassReflection(); $className = $classReflection->getName(); if ($classReflection->isAnonymous()) { $displayName = 'anonymous class'; @@ -43,13 +43,17 @@ public function processNode(Node $node, Scope $scope): array } return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($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 with bound type %%s is not supported.', $displayName), + sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName), + sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName), ); } diff --git a/src/Rules/Generics/CrossCheckInterfacesHelper.php b/src/Rules/Generics/CrossCheckInterfacesHelper.php index 65f5e8d86f..25b57e10e6 100644 --- a/src/Rules/Generics/CrossCheckInterfacesHelper.php +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -2,16 +2,20 @@ namespace PHPStan\Rules\Generics; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; +use function array_key_exists; +use function sprintf; -class CrossCheckInterfacesHelper +#[AutowiredService] +final class CrossCheckInterfacesHelper { /** - * @return RuleError[] + * @return list */ public function check(ClassReflection $classReflection): array { @@ -41,8 +45,8 @@ public function check(ClassReflection $classReflection): array $name, $interface->getName(), $type->describe(VerbosityLevel::value()), - $otherType->describe(VerbosityLevel::value()) - ))->build(); + $otherType->describe(VerbosityLevel::value()), + ))->identifier('generics.interfaceConflict')->build(); } continue; } diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php new file mode 100644 index 0000000000..67733187e3 --- /dev/null +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -0,0 +1,89 @@ + + */ +#[RegisteredRule(level: 2)] +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..29e9231f80 --- /dev/null +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -0,0 +1,47 @@ + + */ +#[RegisteredRule(level: 2)] +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 6e07cbc4c5..b25a263110 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -4,22 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 @@ -29,19 +28,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if ($functionReflection === null) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $functionName = $functionReflection->getName(); return $this->varianceCheck->checkParametersAcceptor( - ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()), + $functionReflection, sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), + sprintf('in param-out type of parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), sprintf('in return type of function %s()', $functionName), sprintf('in function %s()', $functionName), - false + false, + false, + 'function', ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 03d59f3397..29e9f927ec 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -4,28 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 @@ -41,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionName = (string) $node->namespacedName; @@ -50,19 +48,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.', $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() 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 c97a8043cc..56aff3b36f 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -2,85 +2,95 @@ namespace PHPStan\Rules\Generics; +use PhpParser\Node; use PhpParser\Node\Name; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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; + +#[AutowiredService] +final class GenericAncestorsCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - private bool $checkGenericClassInNonGenericObjectType; - - /** @var string[] */ - private array $skipCheckGenericClasses; - /** * @param string[] $skipCheckGenericClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - GenericObjectTypeCheck $genericObjectTypeCheck, - VarianceCheck $varianceCheck, - bool $checkGenericClassInNonGenericObjectType, - array $skipCheckGenericClasses = [] + private ReflectionProvider $reflectionProvider, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private VarianceCheck $varianceCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + #[AutowiredParameter(ref: '%featureToggles.skipCheckGenericClasses%')] + private array $skipCheckGenericClasses, + #[AutowiredParameter] + private bool $checkMissingTypehints, ) { - $this->reflectionProvider = $reflectionProvider; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->varianceCheck = $varianceCheck; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; - $this->skipCheckGenericClasses = $skipCheckGenericClasses; } /** - * @param array<\PhpParser\Node\Name> $nameNodes - * @param array<\PHPStan\Type\Type> $ancestorTypes - * @return \PHPStan\Rules\RuleError[] + * @param array $nameNodes + * @param array $ancestorTypes + * @return 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; @@ -93,29 +103,63 @@ 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)) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('class.notFound') + ->build(); + continue; + } + + if ($referencedClass === $ancestorType->getClassName()) { continue; } - $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass))->build(); + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('generics.trait') + ->build(); } - $variance = TemplateTypeVariance::createInvariant(); + $variance = TemplateTypeVariance::createStatic(); $messageContext = sprintf( $invalidVarianceMessage, - $ancestorType->describe(VerbosityLevel::typeOnly()) + $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; @@ -129,11 +173,25 @@ public function check( continue; } + $templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + continue; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $genericClassInNonGenericObjectType, $unusedName, - implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())) - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $templateTypesList, + )) + ->identifier('missingType.generics') + ->build(); } } diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index 16eea0ed66..83614b364a 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,31 +2,44 @@ namespace PHPStan\Rules\Generics; +use PHPStan\DependencyInjection\AutowiredService; +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 ReflectionClass; +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 +#[AutowiredService] +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,43 +49,95 @@ 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(); } - $templateTypesCount = count($templateTypes); for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; } $templateType = $templateTypes[$i]; - $boundType = TemplateTypeHelper::resolveToBounds($templateType); $genericTypeType = $genericTypeTypes[$i]; + + $genericTypeVariance = $genericTypeVariances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($templateType instanceof TemplateType && !$genericTypeVariance->invariant()) { + if ($genericTypeVariance->equals($templateType->getVariance())) { + if ( + // allow ReflectionClass + // so that same code works for PHP 8.3 and 8.4+ + $classReflection->getName() !== ReflectionClass::class + || $templateType->getName() !== 'T' + ) { + $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; @@ -83,18 +148,28 @@ public function check( continue; } - $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes($templateTypes[$j], $map); + $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes( + $templateTypes[$j], + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + ); } continue; } + if ($genericTypeVariance->bivariant()) { + continue; + } + $messages[] = RuleErrorBuilder::message(sprintf( $typeIsNotSubtypeMessage, $genericTypeType->describe(VerbosityLevel::typeOnly()), $genericType->describe(VerbosityLevel::typeOnly()), $templateType->describe(VerbosityLevel::typeOnly()), - $classReflection->getDisplayName(false) - ))->build(); + $classLikeDescription, + $classReflection->getDisplayName(false), + ))->identifier('generics.notSubtype')->build(); } } @@ -102,17 +177,16 @@ public function check( } /** - * @param \PHPStan\Type\Type $phpDocType - * @return \PHPStan\Type\Generic\GenericObjectType[] + * @return list */ private function getGenericTypes(Type $phpDocType): array { $genericObjectTypes = []; TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $resolvedType = TemplateTypeHelper::resolveToBounds($type); - if (!$resolvedType instanceof GenericObjectType) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) { + throw new ShouldNotHappenException(); } $genericObjectTypes[] = $resolvedType; $traverse($type); diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index fad7ac5ba4..af4f95c64e 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -4,35 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 + * @implements Rule */ -class InterfaceAncestorsRule implements Rule +#[RegisteredRule(level: 2)] +final class InterfaceAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - - private CrossCheckInterfacesHelper $crossCheckInterfacesHelper; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck, - CrossCheckInterfacesHelper $crossCheckInterfacesHelper + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; - $this->crossCheckInterfacesHelper = $crossCheckInterfacesHelper; } public function getNodeType(): string @@ -46,52 +40,33 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Interface_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $interfaceName = $classReflection->getName(); - $extendsTags = []; - $implementsTags = []; - $docComment = $originalNode->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); - } - $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); $extendsErrors = $this->genericAncestorsCheck->check( $originalNode->extends, - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), + 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 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.', + 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', + 'Generic type %s in PHPDoc tag @extends does not specify all template types of %s %s: %s', + 'Generic type %s in PHPDoc tag @extends specifies %d template types, but %s %s supports only %d: %s', + 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), - sprintf('in extended type %%s of interface %s', $escapedInterfaceName) + sprintf('in extended type %%s of interface %s', $escapedInterfaceName), ); $implementsErrors = $this->genericAncestorsCheck->check( [], - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), + 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), '', '', @@ -100,7 +75,8 @@ public function processNode(Node $node, Scope $scope): array '', '', '', - '' + '', + '', ); foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index b26ab74fea..e0f6197223 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -4,65 +4,53 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 []; } - - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $interfaceName = (string) $node->namespacedName; - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); + $interfaceName = $classReflection->getName(); $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($interfaceName), - $resolvedPhpDoc->getTemplateTags(), + $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 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 0b96397f2a..857ba59c17 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -4,23 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 @@ -30,17 +28,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - return []; - } + $method = $node->getMethodReflection(); return $this->varianceCheck->checkParametersAcceptor( - ParametersAcceptorSelector::selectSingle($method->getVariants()), + $method, sprintf('in parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), + sprintf('in param-out type of parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), sprintf('in return type of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), sprintf('in method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), - $method->getName() === '__construct' || $method->isStatic() + $method->isStatic(), + $method->isPrivate() || $method->getName() === '__construct', + 'method', ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeCheck.php b/src/Rules/Generics/MethodTagTemplateTypeCheck.php new file mode 100644 index 0000000000..31eb219ad8 --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeCheck.php @@ -0,0 +1,85 @@ + + */ + 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..e0d21cf492 --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -0,0 +1,44 @@ + + */ +#[RegisteredRule(level: 2)] +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..b1661b5ab7 --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeTraitRule.php @@ -0,0 +1,54 @@ + + */ +#[RegisteredRule(level: 2)] +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 09e7b26b54..6e2af53a41 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -4,30 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 @@ -43,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -51,23 +50,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.', $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() 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(); @@ -76,7 +79,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..39a4bbd78d --- /dev/null +++ b/src/Rules/Generics/PropertyVarianceRule.php @@ -0,0 +1,57 @@ + + */ +#[RegisteredRule(level: 2)] +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 5286a2625b..be58cefefe 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,147 +3,220 @@ namespace PHPStan\Rules\Generics; use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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\IterableType; +use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; -use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; +use function array_merge; +use function get_class; +use function sprintf; -class TemplateTypeCheck +#[AutowiredService] +final class TemplateTypeCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private GenericObjectTypeCheck $genericObjectTypeCheck; - - private TypeAliasResolver $typeAliasResolver; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - TypeAliasResolver $typeAliasResolver, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private TypeAliasResolver $typeAliasResolver, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->typeAliasResolver = $typeAliasResolver; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } /** - * @param \PhpParser\Node $node - * @param TemplateTypeScope $templateTypeScope - * @param array $templateTags - * @return \PHPStan\Rules\RuleError[] + * @param array $templateTags + * @return 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 ($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)); + + $boundTypeClass = get_class($boundType); + if ( + $boundTypeClass !== MixedType::class + && $boundTypeClass !== ConstantArrayType::class + && $boundTypeClass !== ArrayType::class + && $boundTypeClass !== ConstantStringType::class + && $boundTypeClass !== StringType::class + && $boundTypeClass !== ConstantIntegerType::class + && $boundTypeClass !== IntegerType::class + && $boundTypeClass !== FloatType::class + && $boundTypeClass !== BooleanType::class + && $boundTypeClass !== ObjectWithoutClassType::class + && $boundTypeClass !== ObjectType::class + && $boundTypeClass !== ObjectShapeType::class + && $boundTypeClass !== GenericObjectType::class + && $boundTypeClass !== KeyOfType::class + && $boundTypeClass !== IterableType::class + && $boundTypeClass !== NullType::class + && !$boundType instanceof UnionType + && !$boundType instanceof IntersectionType + && !$boundType instanceof TemplateType + ) { + $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.notSupportedBound') + ->build(); } - TypeTraverser::map($templateTag->getBound(), static function (Type $type, callable $traverse) use (&$messages, $notSupportedBoundMessage, $templateTagName): Type { - $boundClass = get_class($type); - if ( - $boundClass === MixedType::class - || $boundClass === ConstantArrayType::class - || $boundClass === ArrayType::class - || $boundClass === StringType::class - || $boundClass === IntegerType::class - || $boundClass === FloatType::class - || $boundClass === BooleanType::class - || $boundClass === ObjectWithoutClassType::class - || $boundClass === ObjectType::class - || $boundClass === GenericObjectType::class - || $type instanceof UnionType - || $type instanceof TemplateType - ) { - return $traverse($type); - } - - $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $type->describe(VerbosityLevel::typeOnly())))->build(); - - return $type; - }); - $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); $genericObjectErrors = $this->genericObjectTypeCheck->check( $boundType, - sprintf('PHPDoc tag @template %s bound contains generic type %%s but class %%s is not generic.', $escapedTemplateTagName), - sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of class %%s: %%s', $escapedTemplateTagName), - sprintf('PHPDoc tag @template %s bound 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 is not subtype of template type %%s of class %%s.', $escapedTemplateTagName) + 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($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 3f756febf4..34d4081bc0 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -4,28 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +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 @@ -41,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $traitName = (string) $node->namespacedName; @@ -50,19 +48,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.', $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 with bound type %%s is not supported.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), + sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s does not have a default type but follows an optional @template %%s.', $escapedTraitName), ); } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index 49ff292ece..9b94857f1d 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -4,29 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function sprintf; +use function strtolower; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse> + * @implements Rule */ -class UsedTraitsRule implements Rule +#[RegisteredRule(level: 2)] +final class UsedTraitsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private FileTypeMapper $fileTypeMapper, + private GenericAncestorsCheck $genericAncestorsCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string @@ -37,7 +38,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $className = $scope->getClassReflection()->getName(); @@ -53,33 +54,37 @@ public function processNode(Node $node, Scope $scope): array $className, $traitName, null, - $docComment->getText() + $docComment->getText(), ); $useTags = $resolvedPhpDoc->getUsesTags(); } - $description = sprintf('class %s', SprintfHelper::escapeFormatString($className)); - $typeDescription = 'class'; + $typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription()); + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className)); if ($traitName !== null) { - $description = sprintf('trait %s', SprintfHelper::escapeFormatString($traitName)); $typeDescription = 'trait'; + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($traitName)); } + $escapedDescription = SprintfHelper::escapeFormatString($description); + $upperCaseDescription = ucfirst($description); + $escapedUpperCaseDescription = SprintfHelper::escapeFormatString($upperCaseDescription); + return $this->genericAncestorsCheck->check( $node->traits, - array_map(static function (UsesTag $tag): Type { - return $tag->getType(); - }, $useTags), - sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)), - sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)), - sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription), - 'PHPDoc tag @use contains generic type %s but trait %s is not generic.', - 'Generic type %s in PHPDoc tag @use does not specify all template types of trait %s: %s', - 'Generic type %s in PHPDoc tag @use specifies %d template types, but trait %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of trait %s.', + 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', ucfirst($description)), - sprintf('in used type %%s of %s', $description) + sprintf('%s uses generic trait %%s but does not specify its types: %%s', $escapedUpperCaseDescription), + sprintf('in used type %%s of %s', $escapedDescription), ); } diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index e8a95fd3c3..cd8d7192bf 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,38 +2,36 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Rules\RuleError; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class VarianceCheck { - /** @return RuleError[] */ + /** + * @param 'function'|'method' $identifier + * @return list + */ public function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + ExtendedParametersAcceptor $parametersAcceptor, string $parameterTypeMessage, + string $parameterOutTypeMessage, string $returnTypeMessage, string $generalMessage, - bool $isStatic + bool $isStatic, + bool $isPrivate, + string $identifier, ): array { $errors = []; - foreach ($parametersAcceptor->getParameters() as $parameterReflection) { - $variance = $isStatic - ? TemplateTypeVariance::createStatic() - : TemplateTypeVariance::createContravariant(); - $type = $parameterReflection->getType(); - $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); - foreach ($this->check($variance, $type, $message) as $error) { - $errors[] = $error; - } - } - foreach ($parametersAcceptor->getTemplateTypeMap()->getTypes() as $templateType) { if (!$templateType instanceof TemplateType || $templateType->getScope()->getFunctionName() === null @@ -45,20 +43,44 @@ public function checkParametersAcceptor( $errors[] = RuleErrorBuilder::message(sprintf( 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type %s in %s.', $templateType->getName(), - $generalMessage - ))->build(); + $generalMessage, + ))->identifier(sprintf('%s.variance', $identifier))->build(); + } + + if ($isPrivate) { + return $errors; + } + + $covariant = TemplateTypeVariance::createCovariant(); + $parameterVariance = TemplateTypeVariance::createContravariant(); + + foreach ($parametersAcceptor->getParameters() as $parameterReflection) { + $type = $parameterReflection->getType(); + $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); + foreach ($this->check($parameterVariance, $type, $message) as $error) { + $errors[] = $error; + } + + $paramOutType = $parameterReflection->getOutType(); + if ($paramOutType === null) { + continue; + } + + $outMessage = sprintf($parameterOutTypeMessage, $parameterReflection->getName()); + foreach ($this->check($covariant, $paramOutType, $outMessage) as $error) { + $errors[] = $error; + } } - $variance = TemplateTypeVariance::createCovariant(); $type = $parametersAcceptor->getReturnType(); - foreach ($this->check($variance, $type, $returnTypeMessage) as $error) { + foreach ($this->check($covariant, $type, $returnTypeMessage) as $error) { $errors[] = $error; } return $errors; } - /** @return RuleError[] */ + /** @return list */ public function check(TemplateTypeVariance $positionVariance, Type $type, string $messageContext): array { $errors = []; @@ -75,8 +97,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..863542c96a --- /dev/null +++ b/src/Rules/Ignore/IgnoreParseErrorRule.php @@ -0,0 +1,49 @@ + + */ +#[RegisteredRule(level: 0)] +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..4994a09580 --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalUsageHelper.php @@ -0,0 +1,33 @@ +getNamespace(); + if ($currentNamespace === null) { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return true; + } + + return $classReflection->getName() !== $name; + } + + $currentNamespace = explode('\\', $currentNamespace)[0]; + $namespace = array_slice(explode('\\', $name), 0, -1)[0] ?? null; + + return !str_starts_with($namespace . '\\', $currentNamespace . '\\'); + } + +} diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index a1a1db0607..28b7780543 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -5,41 +5,43 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; - -class IssetCheck +use function is_string; +use function sprintf; +use function str_starts_with; + +/** + * @phpstan-type ErrorIdentifier = 'empty'|'isset'|'nullCoalesce' + */ +#[AutowiredService] +final class IssetCheck { - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private bool $checkAdvancedIsset; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder, - bool $checkAdvancedIsset, - bool $treatPhpDocTypesAsCertain + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + #[AutowiredParameter] + private bool $checkAdvancedIsset, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, ) { - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->checkAdvancedIsset = $checkAdvancedIsset; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } /** + * @param ErrorIdentifier $identifier * @param callable(Type): ?string $typeMessageCallback */ - public function check(Expr $expr, Scope $scope, string $operatorDescription, callable $typeMessageCallback, ?RuleError $error = null): ?RuleError + public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { + // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $scope->hasVariableType($expr->name); if ($hasVariable->maybe()) { @@ -52,14 +54,21 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - return $this->generateError( - $scope->getVariableType($expr->name), - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), - $typeMessageCallback - ); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr); + if (!$type instanceof NeverType) { + return $this->generateError( + $type, + sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), + $typeMessageCallback, + $identifier, + 'variable', + ); + } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } return $error; @@ -67,19 +76,15 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); - if (!$type->isOffsetAccessible()->yes()) { - return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription); - } - if ($hasOffsetValue->no()) { - if ($error !== null) { - return $error; - } - if (!$this->checkAdvancedIsset) { return null; } @@ -89,35 +94,27 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal '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 ($error !== null) { - return $error; - } - + if ($hasOffsetValue->yes() || $scope->hasExpressionType($expr)->yes()) { if (!$this->checkAdvancedIsset) { return null; } - $error = $this->generateError($type->getOffsetValueType($dimType), sprintf( + $error ??= $this->generateError($type->getOffsetValueType($dimType), sprintf( 'Offset %s on %s %s always exists and', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - ), $typeMessageCallback); + $operatorDescription, + ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -130,11 +127,11 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if ($propertyReflection === null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -142,43 +139,73 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if (!$propertyReflection->isNative()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } - $nativeType = $propertyReflection->getNativeType(); - if (!$nativeType instanceof MixedType) { - if (!$scope->isSpecified($expr)) { + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if ( + $expr instanceof Node\Expr\PropertyFetch + && $expr->name instanceof Node\Identifier + && $expr->var instanceof Expr\Variable + && $expr->var->name === 'this' + && $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes() + ) { + return $this->generateError( + $propertyReflection->getNativeType(), + sprintf( + '%s %s', + $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr), + $operatorDescription, + ), + static function (Type $type) use ($typeMessageCallback): ?string { + $originalMessage = $typeMessageCallback($type); + if ($originalMessage === null) { + return null; + } + + if (str_starts_with($originalMessage, 'is not')) { + return sprintf('%s nor uninitialized', $originalMessage); + } + + return sprintf('%s and initialized', $originalMessage); + }, + $identifier, + 'initializedProperty', + ); + } + + if (!$scope->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { return $error; } if (!$this->checkAdvancedIsset) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -187,16 +214,18 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $error = $this->generateError( $propertyReflection->getWritableType(), sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription), - $typeMessageCallback + $typeMessageCallback, + $identifier, + 'property', ); if ($error !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -211,10 +240,36 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - return $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription), $typeMessageCallback); + $error = $this->generateError( + $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr), + sprintf('Expression %s', $operatorDescription), + $typeMessageCallback, + $identifier, + 'expr', + ); + if ($error !== null) { + return $error; + } + + if ($expr instanceof Expr\NullsafePropertyFetch) { + if ($expr->name instanceof Node\Identifier) { + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->%s" %s is unnecessary. Use -> instead.', $expr->name->name, $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); + } + + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->(Expression)" %s is unnecessary. Use -> instead.', $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); + } + + return null; } - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription): ?RuleError + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError { if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $scope->hasVariableType($expr->name); @@ -222,19 +277,21 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $scope->getType($expr->var); - $dimType = $scope->getType($expr->dim); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if (!$hasOffsetValue->no()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } return RuleErrorBuilder::message( @@ -242,17 +299,17 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri 'Offset %s on %s %s does not exist.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - ) - )->build(); + $operatorDescription, + ), + )->identifier(sprintf('%s.offset', $identifier))->build(); } if ($expr instanceof Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -260,8 +317,10 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri /** * @param callable(Type): ?string $typeMessageCallback + * @param ErrorIdentifier $identifier + * @param 'variable'|'offset'|'property'|'expr'|'initializedProperty' $identifierSecondPart */ - private function generateError(Type $type, string $message, callable $typeMessageCallback): ?RuleError + private function generateError(Type $type, string $message, callable $typeMessageCallback, string $identifier, string $identifierSecondPart): ?IdentifierRuleError { $typeMessage = $typeMessageCallback($type); if ($typeMessage === null) { @@ -269,8 +328,8 @@ private function generateError(Type $type, string $message, callable $typeMessag } return RuleErrorBuilder::message( - sprintf('%s %s.', $message, $typeMessage) - )->build(); + 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 index 987ae48f5e..e07d63a5b7 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -5,13 +5,19 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Parser\ParentStmtTypesVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_reverse; +use function in_array; +use function sprintf; /** * @implements Rule */ -class ContinueBreakInLoopRule implements Rule +#[RegisteredRule(level: 0)] +final class ContinueBreakInLoopRule implements Rule { public function getNodeType(): string @@ -25,44 +31,52 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->num instanceof Node\Scalar\LNumber) { + if (!$node->num instanceof Node\Scalar\Int_) { $value = 1; } else { $value = $node->num->value; } - $parent = $node->getAttribute('parent'); - while ($value > 0) { - if ( - $parent === null - || $parent instanceof Stmt\Function_ - || $parent instanceof Stmt\ClassMethod - || $parent instanceof Node\Expr\Closure - ) { + $parentStmtTypes = array_reverse($node->getAttribute(ParentStmtTypesVisitor::ATTRIBUTE_NAME)); + foreach ($parentStmtTypes as $parentStmtType) { + if ($parentStmtType === Stmt\Case_::class) { + continue; + } + if ($parentStmtType === Node\Expr\Closure::class) { return [ RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', - $node instanceof Stmt\Continue_ ? 'continue' : 'break' - ))->nonIgnorable()->build(), + $node instanceof Stmt\Continue_ ? 'continue' : 'break', + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), ]; } - if ( - $parent instanceof Stmt\For_ - || $parent instanceof Stmt\Foreach_ - || $parent instanceof Stmt\Do_ - || $parent instanceof Stmt\While_ - ) { + if (in_array($parentStmtType, [ + Stmt\For_::class, + Stmt\Foreach_::class, + Stmt\Do_::class, + Stmt\While_::class, + Stmt\Switch_::class, + ], true)) { $value--; } - if ($parent instanceof Stmt\Case_) { - $value--; - $parent = $parent->getAttribute('parent'); - if (!$parent instanceof Stmt\Switch_) { - throw new \PHPStan\ShouldNotHappenException(); - } + if ($value === 0) { + break; } + } - $parent = $parent->getAttribute('parent'); + if ($value > 0) { + return [ + RuleErrorBuilder::message(sprintf( + 'Keyword %s used outside of a loop or a switch statement.', + $node instanceof Stmt\Continue_ ? 'continue' : 'break', + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), + ]; } return []; diff --git a/src/Rules/Keywords/DeclareStrictTypesRule.php b/src/Rules/Keywords/DeclareStrictTypesRule.php new file mode 100644 index 0000000000..52ad44c098 --- /dev/null +++ b/src/Rules/Keywords/DeclareStrictTypesRule.php @@ -0,0 +1,82 @@ + + */ +#[RegisteredRule(level: 0)] +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..b0d4def309 --- /dev/null +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -0,0 +1,143 @@ + + */ +#[RegisteredRule(level: 0)] +final class RequireFileExistsRule implements Rule +{ + + public function __construct( + #[AutowiredParameter] + 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..4a55509947 --- /dev/null +++ b/src/Rules/LazyRegistry.php @@ -0,0 +1,73 @@ + $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..3a039e6f2a 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -4,13 +4,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function in_array; +use function sprintf; /** * @implements Rule */ -class AbstractMethodInNonAbstractClassRule implements Rule +#[RegisteredRule(level: 0)] +final class AbstractMethodInNonAbstractClassRule implements Rule { public function getNodeType(): string @@ -21,21 +26,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..4884bbd956 --- /dev/null +++ b/src/Rules/Methods/AbstractPrivateMethodRule.php @@ -0,0 +1,60 @@ + */ +#[RegisteredRule(level: 0)] +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 +#[RegisteredRule(level: 0)] +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 @@ -53,126 +36,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.', SprintfHelper::escapeFormatString($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 !== null) { - 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); $declaringClass = $methodReflection->getDeclaringClass(); $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors = []; - if (!$scope->canCallMethod($methodReflection)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Call to %s method %s() of class %s.', - $methodReflection->isPrivate() ? 'private' : 'protected', - $methodReflection->getName(), - $declaringClass->getDisplayName() - ))->build(); - } - $errors = array_merge($errors, $this->check->check( + return array_merge($errors, $this->parametersCheck->check( ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ), $scope, $declaringClass->isBuiltin(), $node, - [ - 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of method ' . $messagesMethodName . ' expects %s, %s given.', - 'Result of method ' . $messagesMethodName . ' (void) is used.', - 'Parameter %s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, - 'Missing parameter $%s in call to method ' . $messagesMethodName . '.', - 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', - 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', - ] + 'method', + $methodReflection->acceptsNamedArguments(), + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of method ' . $messagesMethodName . ' expects %s, %s given.', + 'Result of method ' . $messagesMethodName . ' (void) is used.', + '%s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, + 'Missing parameter $%s in call to method ' . $messagesMethodName . '.', + 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', + 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', + '%s of method ' . $messagesMethodName . ' contains unresolvable type.', + 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); - - 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 index 089cd81613..327b2ba473 100644 --- a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -6,13 +6,16 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class CallPrivateMethodThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class CallPrivateMethodThroughStaticRule implements Rule { public function getNodeType(): string @@ -53,8 +56,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Unsafe call to private method %s::%s() through static::.', $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), + $method->getName(), + ))->identifier('staticClassAccess.privateMethod')->build(), ]; } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 87fda9718c..55537f84f0 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -3,64 +3,31 @@ 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\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; -use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\VerbosityLevel; +use PHPStan\Rules\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 +#[RegisteredRule(level: 0)] +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 @@ -70,241 +37,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(), - ]; - } - $classType = $scope->resolveTypeByName($class); - } elseif ($lowercasedClassName === 'parent') { - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Calling %s::%s() outside of class scope.', - $className, - $methodName - ))->build(), - ]; - } - $currentClassReflection = $scope->getClassReflection(); - if ($currentClassReflection->getParentClass() === null) { - return [ - RuleErrorBuilder::message(sprintf( - '%s::%s() calls parent::%s() but %s does not extend any class.', - $scope->getClassReflection()->getDisplayName(), - $scope->getFunctionName(), - $methodName, - $scope->getClassReflection()->getDisplayName() - ))->build(), - ]; - } - - if ($scope->getFunctionName() === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classType = $scope->resolveTypeByName($class); - } else { - if (!$this->reflectionProvider->hasClass($className)) { - if ($scope->isInClassExists($className)) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on an unknown class %s.', - $methodName, - $className - ))->discoveringSymbolsTip()->build(), - ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); - } - - $classType = $scope->resolveTypeByName($class); - } - - $classReflection = $classType->getClassReflection(); - if ($classReflection !== null && $classReflection->hasNativeMethod($methodName) && $lowercasedClassName !== 'static') { - $nativeMethodReflection = $classReflection->getNativeMethod($methodName); - if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { - $isAbstract = $nativeMethodReflection->isAbstract(); - } - } + 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.', SprintfHelper::escapeFormatString($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(); + $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))); } } - if ($classType instanceof GenericClassStringType) { - $classType = $classType->getGenericType(); - if (!(new ObjectWithoutClassType())->isSuperTypeOf($classType)->yes()) { - return []; - } - } elseif ((new StringType())->isSuperTypeOf($classType)->yes()) { - return []; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); } - $typeForDescribe = $classType; - if ($classType instanceof ThisType) { - $typeForDescribe = $classType->getStaticObjectType(); - } - $classType = TypeCombinator::remove($classType, new StringType()); - - if (!$classType->canCallMethods()->yes()) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Cannot call static method %s() on %s.', - $methodName, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) - ))->build(), - ]); - } - - if (!$classType->hasMethod($methodName)->yes()) { - if (!$this->reportMagicMethods) { - $directClassNames = TypeUtils::getDirectClassNames($classType); - foreach ($directClassNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasNativeMethod('__callStatic')) { - return []; - } - } - } - - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to an undefined static method %s::%s().', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $methodName - ))->build(), - ]); - } - - $method = $classType->getMethod($methodName, $scope); - if (!$method->isStatic()) { - $function = $scope->getFunction(); - if ( - !$function instanceof MethodReflection - || $function->isStatic() - || !$scope->isInClass() - || ( - $classType instanceof TypeWithClassName - && $scope->getClassReflection()->getName() !== $classType->getClassName() - && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) - ) - ) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Static call to instance method %s::%s().', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]); - } - } - - if (!$scope->canCallMethod($method)) { - $errors = array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s %s() of class %s.', - $method->isPrivate() ? 'private' : 'protected', - $method->isStatic() ? 'static method' : 'method', - $method->getName(), - $method->getDeclaringClass()->getDisplayName() - ))->build(), - ]); - } + 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 = SprintfHelper::escapeFormatString(sprintf( - '%s %s', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - )); $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->getArgs(), - $method->getVariants() + $method->getVariants(), + $method->getNamedArgumentsVariants(), ), $scope, $method->getDeclaringClass()->isBuiltin(), $node, - [ - $displayMethodName . ' invoked with %d parameter, %d required.', - $displayMethodName . ' invoked with %d parameters, %d required.', - $displayMethodName . ' invoked with %d parameter, at least %d required.', - $displayMethodName . ' invoked with %d parameters, at least %d required.', - $displayMethodName . ' invoked with %d parameter, %d-%d required.', - $displayMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $lowercasedMethodName . ' expects %s, %s given.', - 'Result of ' . $lowercasedMethodName . ' (void) is used.', - 'Parameter %s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, - 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', - 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', - 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', - ] + 'staticMethod', + $method->acceptsNamedArguments(), + $displayMethodName . ' invoked with %d parameter, %d required.', + $displayMethodName . ' invoked with %d parameters, %d required.', + $displayMethodName . ' invoked with %d parameter, at least %d required.', + $displayMethodName . ' invoked with %d parameters, at least %d required.', + $displayMethodName . ' invoked with %d parameter, %d-%d required.', + $displayMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $lowercasedMethodName . ' expects %s, %s given.', + 'Result of ' . $lowercasedMethodName . ' (void) is used.', + '%s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $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 index a42bc59fa5..8377d4cfcd 100644 --- a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -4,37 +4,40 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToConstructorStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToConstructorStatementWithoutSideEffectsRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + ) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\New_) { + $instantiation = $node->getOriginalExpr(); + if (!$instantiation instanceof Node\Expr\New_) { return []; } - $instantiation = $node->expr; if (!$instantiation->class instanceof Node\Name) { return []; } @@ -46,31 +49,31 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->hasConstructor()) { - return []; - } - - $constructor = $classReflection->getConstructor(); - if ($constructor->hasSideEffects()->no()) { - $throwsType = $constructor->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; - } - - $methodResult = $scope->getType($instantiation); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } - return [ RuleErrorBuilder::message(sprintf( - 'Call to %s::%s() on a separate line has no effect.', + 'Call to new %s() on a separate line has no effect.', $classReflection->getDisplayName(), - $constructor->getName() - ))->build(), + ))->identifier('new.resultUnused')->build(), ]; } - return []; + $constructor = $classReflection->getConstructor(); + if (count($constructor->getAsserts()->getAsserts()) > 0) { + return []; + } + + $methodResult = $scope->getType($instantiation); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() on a separate line has no effect.', + $classReflection->getDisplayName(), + $constructor->getName(), + ))->identifier('new.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..e0a5d2ff4e --- /dev/null +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -0,0 +1,84 @@ + + */ +#[RegisteredRule(level: 0)] +final class CallToMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct(private 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 + && !$node->expr instanceof Node\Expr\NullsafeMethodCall + ) { + return []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + $methodName = $funcCall->name->toString(); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->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 []; + } + + $method = $calledOnType->getMethod($methodName, $scope); + + if (!$method->mustUseReturnValue()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line discards return value.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.resultDiscarded')->build(), + ]; + } + +} diff --git a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php index 0ade6b4c03..e0a99028c2 100644 --- a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -3,42 +3,44 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToMethodStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToMethodStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if ($node->expr instanceof Node\Expr\NullsafeMethodCall) { - $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($node->expr->var, new Node\Expr\ConstFetch(new Node\Name('null')))); - } elseif (!$node->expr instanceof Node\Expr\MethodCall) { + $methodCall = $node->getOriginalExpr(); + if ($methodCall instanceof Node\Expr\NullsafeMethodCall) { + $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($methodCall->var, new Node\Expr\ConstFetch(new Node\Name('null')))); + } elseif (!$methodCall instanceof Node\Expr\MethodCall) { return []; } - $methodCall = $node->expr; if (!$methodCall->name instanceof Node\Identifier) { return []; } @@ -46,11 +48,9 @@ public function processNode(Node $node, Scope $scope): array $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $methodCall->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $methodCall->var), '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); $calledOnType = $typeResult->getType(); if ($calledOnType instanceof ErrorType) { @@ -64,29 +64,24 @@ static function (Type $type) use ($methodName): bool { return []; } - $method = $calledOnType->getMethod($methodName, $scope); - if ($method->hasSideEffects()->no()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; - } - - $methodResult = $scope->getType($methodCall); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } + $methodResult = $scope->getType($methodCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; + } - return [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s::%s() on a separate line has no effect.', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]; + $method = $calledOnType->getMethod($methodName, $scope); + if (count($method->getAsserts()->getAsserts()) > 0) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..349eeeace7 --- /dev/null +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -0,0 +1,97 @@ + + */ +#[RegisteredRule(level: 0)] +final class CallToStaticMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + 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\StaticCall) { + return []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + + $methodName = $funcCall->name->toString(); + if ($funcCall->class instanceof Node\Name) { + $className = $scope->resolveName($funcCall->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $calledOnType = new ObjectType($className); + } else { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->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 (!$method->mustUseReturnValue()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line discards return value.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticMethod.resultDiscarded')->build(), + ]; + } + +} diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php index 70d25300bb..2c80a25856 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -3,7 +3,10 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,39 +15,36 @@ use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; +use function count; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - ReflectionProvider $reflectionProvider + private RuleLevelHelper $ruleLevelHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\StaticCall) { + $staticCall = $node->getOriginalExpr(); + if (!$staticCall instanceof Node\Expr\StaticCall) { return []; } - $staticCall = $node->expr; if (!$staticCall->name instanceof Node\Identifier) { return []; } @@ -60,11 +60,9 @@ public function processNode(Node $node, Scope $scope): array } else { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $staticCall->class, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $staticCall->class), '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); $calledOnType = $typeResult->getType(); if ($calledOnType instanceof ErrorType) { @@ -90,28 +88,23 @@ static function (Type $type) use ($methodName): bool { return []; } - if ($method->hasSideEffects()->no()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; - } - - $methodResult = $scope->getType($staticCall); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } + if (count($method->getAsserts()->getAsserts()) > 0) { + return []; + } - return [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s::%s() on a separate line has no effect.', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]; + $methodResult = $scope->getType($staticCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticMethod.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/ConsistentConstructorDeclarationRule.php b/src/Rules/Methods/ConsistentConstructorDeclarationRule.php new file mode 100644 index 0000000000..a900073d6f --- /dev/null +++ b/src/Rules/Methods/ConsistentConstructorDeclarationRule.php @@ -0,0 +1,50 @@ + */ +#[RegisteredRule(level: 0)] +final class ConsistentConstructorDeclarationRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + if (strtolower($method->getName()) !== '__construct') { + return []; + } + + $classReflection = $node->getClassReflection(); + if (!$classReflection->hasConsistentConstructor()) { + return []; + } + + if ($classReflection->isFinal()) { + return []; + } + + if (!$method->isPrivate()) { + return []; + } + + return [ + RuleErrorBuilder::message('Private constructor cannot be enforced as consistent for child classes.') + ->identifier('consistentConstructor.private') + ->build(), + ]; + } + +} diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php new file mode 100644 index 0000000000..5eace25f3a --- /dev/null +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -0,0 +1,55 @@ + */ +#[RegisteredRule(level: 0)] +final class ConsistentConstructorRule implements Rule +{ + + public function __construct( + private ConsistentConstructorHelper $consistentConstructorHelper, + 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 []; + } + + $parentConstructor = $this->consistentConstructorHelper->findConsistentConstructor($parent); + if ($parentConstructor === null) { + 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..adfaff6c0c --- /dev/null +++ b/src/Rules/Methods/ConstructorReturnTypeRule.php @@ -0,0 +1,65 @@ + + */ +#[RegisteredRule(level: 0)] +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 7632ef1489..11781bb2e4 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -4,22 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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 @@ -29,32 +29,46 @@ 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(); - } - - $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $methodReflection = $node->getMethodReflection(); + $className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()); $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); return $this->check->checkClassMethod( + $scope, $methodReflection, $node->getOriginalNode(), sprintf( 'Parameter $%%s of method %s::%s() has invalid type %%s.', $className, - $methodName + $methodName, ), sprintf( 'Method %s::%s() has invalid return type %%s.', $className, - $methodName + $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('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( + 'Method %s::%s() has invalid @phpstan-self-out type %%s.', + $className, + $methodName, + ), + sprintf( + 'Attribute NoDiscard cannot be used on %%s method %s::%s().', + $className, + $methodName, + ), ); } diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php new file mode 100644 index 0000000000..20ea6ab6fa --- /dev/null +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -0,0 +1,47 @@ + */ +#[RegisteredRule(level: 0)] +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 172b0170ee..ff239d9a2d 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -4,18 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 2)] +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -25,13 +28,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,14 +38,16 @@ 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; } @@ -61,8 +60,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..1a2b49ee38 --- /dev/null +++ b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,24 @@ +extensions ??= $this->container->getServicesByTag(static::EXTENSION_TAG); + } + +} diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 5e501f76d5..56bb6016a1 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Methods; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class MethodAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class MethodAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string { - return Node\Stmt\ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, - \Attribute::TARGET_METHOD, - 'method' + $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..3be2032165 --- /dev/null +++ b/src/Rules/Methods/MethodCallCheck.php @@ -0,0 +1,170 @@ +, 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..395089046f --- /dev/null +++ b/src/Rules/Methods/MethodCallableRule.php @@ -0,0 +1,68 @@ + + */ +#[RegisteredRule(level: 0)] +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..a52dbe86f5 --- /dev/null +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -0,0 +1,412 @@ + + */ + 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/MethodPrototypeFinder.php b/src/Rules/Methods/MethodPrototypeFinder.php new file mode 100644 index 0000000000..0ec7c9a1a6 --- /dev/null +++ b/src/Rules/Methods/MethodPrototypeFinder.php @@ -0,0 +1,100 @@ +getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasNativeMethod($methodName)) { + $method = $immediateInterface->getNativeMethod($methodName); + return [$method, $method->getDeclaringClass(), true]; + } + } + + if ($this->phpVersion->supportsAbstractTraitMethods()) { + foreach ($classReflection->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if ($isAbstract) { + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + return [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $classReflection, + $methodReflection, + $declaringTrait->getName(), + ), + $declaringTrait, + false, + ]; + } + } + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasNativeMethod($methodName)) { + return null; + } + + $method = $parentClass->getNativeMethod($methodName); + if ($method->isPrivate()) { + return null; + } + + $declaringClass = $method->getDeclaringClass(); + if ($declaringClass->hasConstructor()) { + if ($method->getName() === $declaringClass->getConstructor()->getName()) { + $prototype = $method->getPrototype(); + if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) { + $abstract = $prototype->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + return null; + } + } elseif (!$abstract->yes()) { + return null; + } + } + } elseif (strtolower($methodName) === '__construct') { + return null; + } + } + + return [$method, $method->getDeclaringClass(), true]; + } + +} diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 080aa2d58d..2e585ff43e 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -6,12 +6,21 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; @@ -19,25 +28,23 @@ 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 @@ -47,11 +54,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - + $method = $node->getMethodReflection(); $methodName = $method->getName(); if ($methodName === '__construct') { return []; @@ -62,35 +65,54 @@ public function processNode(Node $node, Scope $scope): array if ($method->isPrivate()) { return []; } - $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $errors = []; $declaringClass = $method->getDeclaringClass(); - foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) { + foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { $parentVariants = $parentMethod->getVariants(); if (count($parentVariants) !== 1) { continue; } - $parentParameters = $parentVariants[0]; - if (!$parentParameters instanceof ParametersAcceptorWithPhpDocs) { - continue; - } - - [$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $parameters, $parentParameters); + $parentVariant = $parentVariants[0]; + [$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $method, $parentVariant); if ($returnTypeCompatibility->no() || (!$returnTypeCompatibility->yes() && $this->reportMaybes)) { - $errors[] = RuleErrorBuilder::message(sprintf( + $builder = RuleErrorBuilder::message(sprintf( 'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()', $returnType->describe(VerbosityLevel::value()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() - ))->build(); + $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($declaringClass, $parameters->getParameters(), $parentParameters->getParameters()); + $parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $method->getParameters(), $parentVariant->getParameters()); foreach ($parameterResults as $parameterIndex => [$parameterResult, $parameterType, $parentParameterType]) { if ($parameterResult->yes()) { continue; @@ -98,8 +120,8 @@ public function processNode(Node $node, Scope $scope): array if (!$parameterResult->no() && !$this->reportMaybes) { continue; } - $parameter = $parameters->getParameters()[$parameterIndex]; - $parentParameter = $parentParameters->getParameters()[$parameterIndex]; + $parameter = $method->getParameters()[$parameterIndex]; + $parentParameter = $parentVariant->getParameters()[$parameterIndex]; $errors[] = RuleErrorBuilder::message(sprintf( 'Parameter #%d $%s (%s) of method %s::%s() should be %s with parameter $%s (%s) of method %s::%s()', $parameterIndex + 1, @@ -110,9 +132,9 @@ public function processNode(Node $node, Scope $scope): array $parameterResult->no() ? 'compatible' : 'contravariant', $parentParameter->getName(), $parentParameterType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() - ))->build(); + $parentMethodDeclaringClass->getDisplayName(), + $parentMethod->getName(), + ))->identifier('method.childParameterType')->build(); } } @@ -120,9 +142,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $methodName - * @param \PHPStan\Reflection\ClassReflection $class - * @return \PHPStan\Reflection\MethodReflection[] + * @return list */ private function collectParentMethods(string $methodName, ClassReflection $class): array { @@ -132,7 +152,7 @@ private function collectParentMethods(string $methodName, ClassReflection $class if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { - $parentMethods[] = $parentMethod; + $parentMethods[] = [$parentMethod, $parentMethod->getDeclaringClass()]; } } @@ -141,57 +161,80 @@ private function collectParentMethods(string $methodName, ClassReflection $class continue; } - $parentMethods[] = $interface->getNativeMethod($methodName); + $method = $interface->getNativeMethod($methodName); + $parentMethods[] = [$method, $method->getDeclaringClass()]; + } + + foreach ($class->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if (!$isAbstract) { + continue; + } + + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + $parentMethods[] = [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $class, + $methodReflection, + $declaringTrait->getName(), + ), + $declaringTrait, + ]; } return $parentMethods; } /** - * @param ParametersAcceptorWithPhpDocs $currentVariant - * @param ParametersAcceptorWithPhpDocs $parentVariant * @return array{TrinaryLogic, Type, Type} */ private function checkReturnTypeCompatibility( ClassReflection $declaringClass, - ParametersAcceptorWithPhpDocs $currentVariant, - ParametersAcceptorWithPhpDocs $parentVariant + ExtendedParametersAcceptor $currentVariant, + ExtendedParametersAcceptor $parentVariant, ): array { $returnType = TypehintHelper::decideType( $currentVariant->getNativeReturnType(), - TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType()) + TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType()), ); $originalParentReturnType = TypehintHelper::decideType( $parentVariant->getNativeReturnType(), - TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType()) + TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType()), ); $parentReturnType = $this->transformStaticType($declaringClass, $originalParentReturnType); // Allow adding `void` return type hints when the parent defines no return type - if ($returnType instanceof VoidType && $parentReturnType instanceof MixedType) { + if ($returnType->isVoid()->yes() && $parentReturnType instanceof MixedType) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } // We can return anything - if ($parentReturnType instanceof VoidType) { + if ($parentReturnType->isVoid()->yes()) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } - return [$parentReturnType->isSuperTypeOf($returnType), TypehintHelper::decideType( + return [$parentReturnType->isSuperTypeOf($returnType)->result, TypehintHelper::decideType( $currentVariant->getNativeReturnType(), - $currentVariant->getPhpDocReturnType() + $currentVariant->getPhpDocReturnType(), ), $originalParentReturnType]; } /** - * @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parameters - * @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parentParameters + * @param ExtendedParameterReflection[] $parameters + * @param ExtendedParameterReflection[] $parentParameters * @return array */ private function checkParameterTypeCompatibility( ClassReflection $declaringClass, array $parameters, - array $parentParameters + array $parentParameters, ): array { $parameterResults = []; @@ -203,17 +246,17 @@ private function checkParameterTypeCompatibility( $parameterType = TypehintHelper::decideType( $parameter->getNativeType(), - TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType()) + TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType()), ); $originalParameterType = TypehintHelper::decideType( $parentParameter->getNativeType(), - TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType()) + TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType()), ); $parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType); - $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType), TypehintHelper::decideType( + $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType)->result, TypehintHelper::decideType( $parameter->getNativeType(), - $parameter->getPhpDocType() + $parameter->getPhpDocType(), ), $originalParameterType]; } @@ -223,6 +266,15 @@ private function checkParameterTypeCompatibility( private function transformStaticType(ClassReflection $declaringClass, Type $type): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { + if ($type instanceof GenericStaticType) { + if ($declaringClass->isFinal()) { + $changedType = $type->changeBaseClass($declaringClass)->getStaticObjectType(); + } else { + $changedType = $type->changeBaseClass($declaringClass); + } + return $traverse($changedType); + } + if ($type instanceof StaticType) { if ($declaringClass->isFinal()) { $changedType = new ObjectType($declaringClass->getName()); diff --git a/src/Rules/Methods/MethodVisibilityComparisonHelper.php b/src/Rules/Methods/MethodVisibilityComparisonHelper.php new file mode 100755 index 0000000000..f0e922deec --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityComparisonHelper.php @@ -0,0 +1,53 @@ + */ + 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..485959b362 --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php @@ -0,0 +1,49 @@ + */ +#[RegisteredRule(level: 0)] +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..604afa8666 --- /dev/null +++ b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php @@ -0,0 +1,89 @@ + + */ +#[RegisteredRule(level: 0)] +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 201ce41461..3f4d7dd136 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -5,14 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class MissingMethodImplementationRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingMethodImplementationRule implements Rule { public function getNodeType(): string @@ -34,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array try { $nativeMethods = $classReflection->getNativeReflection()->getMethods(); - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { return []; } foreach ($nativeMethods as $method) { @@ -44,13 +47,19 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $method->getDeclaringClass(); + $classLikeDescription = 'Non-abstract class'; + if ($classReflection->isEnum()) { + $classLikeDescription = 'Enum'; + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Non-abstract class %s contains abstract method %s() from %s %s.', + '%s %s contains abstract method %s() from %s %s.', + $classLikeDescription, $classReflection->getDisplayName(), $method->getName(), $declaringClass->isInterface() ? 'interface' : 'class', - $declaringClass->getName() - ))->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 effd1d6a05..9961a60254 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -4,26 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 6)] +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 +36,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 +63,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 type specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName() - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -73,33 +82,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(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->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 parameter $%s with no signature specified for %s.', + 'Method %s::%s() has %s with no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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 bcacd1bc9c..370f59c6c4 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -4,25 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 6)] +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 +31,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 type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), - $methodReflection->getName() - ))->build(), + $methodReflection->getName(), + ))->identifier('missingType.return')->build(), ]; } @@ -56,8 +58,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(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -66,8 +71,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames) - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($returnType) as $callableType) { @@ -75,8 +82,8 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() return type has no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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..493729a44d --- /dev/null +++ b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php @@ -0,0 +1,86 @@ + + */ +#[RegisteredRule(level: 6)] +final class MissingMethodSelfOutTypeRule implements Rule +{ + + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methodReflection = $node->getMethodReflection(); + $selfOutType = $methodReflection->getSelfOutType(); + + if ($selfOutType === null) { + return []; + } + + $classReflection = $methodReflection->getDeclaringClass(); + $phpDocTagMessage = 'PHPDoc tag @phpstan-self-out'; + + $messages = []; + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($selfOutType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with no value type specified in iterable type %s.', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($selfOutType) as [$name, $genericTypeNames]) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($selfOutType) as $callableType) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with no signature specified for %s.', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index a70bf220f2..5e97ca0bf9 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -4,15 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule */ -class NullsafeMethodCallRule implements Rule +#[RegisteredRule(level: 4)] +final class NullsafeMethodCallRule implements Rule { public function getNodeType(): string @@ -22,18 +24,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 56cdd11762..f1d3044b75 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -3,47 +3,44 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Attribute; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +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_slice; +use function array_merge; +use function count; +use function is_bool; +use function sprintf; +use function strtolower; /** * @implements Rule */ -class OverridingMethodRule implements Rule +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkPhpDocMethodSignatures, + private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, + private MethodPrototypeFinder $methodPrototypeFinder, + #[AutowiredParameter] + private bool $checkMissingOverrideMethodAttribute, ) { - $this->phpVersion = $phpVersion; - $this->methodSignatureRule = $methodSignatureRule; - $this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; } public function getNodeType(): string @@ -53,47 +50,108 @@ 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->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); + if ($prototypeData === null) { if (strtolower($method->getName()) === '__construct') { $parent = $method->getDeclaringClass()->getParentClass(); if ($parent !== null && $parent->hasConstructor()) { $parentConstructor = $parent->getConstructor(); - if ($parentConstructor->isFinal()->yes()) { + if ($parentConstructor->isFinalByKeyword()->yes()) { return $this->addErrors([ RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parent->getDisplayName(), - $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') + ->fixNode($node->getOriginalNode(), function (Node\Stmt\ClassMethod $method) { + $method->attrGroups = $this->filterOverrideAttribute($method->attrGroups); + return $method; + }) + ->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') + ->fixNode($node->getOriginalNode(), static function (Node\Stmt\ClassMethod $method) { + $method->attrGroups[] = new Node\AttributeGroup([ + new Attribute(new Node\Name\FullyQualified('Override')), + ]); + + return $method; + }) + ->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()) { @@ -102,39 +160,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(); @@ -144,251 +191,75 @@ public function processNode(Node $node, Scope $scope): array $prototypeVariant = $prototypeVariants[0]; - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $methodParameters = $methodVariant->getParameters(); - - $prototypeAfterVariadic = false; - foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) { - if (!array_key_exists($i, $methodParameters)) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() overrides method %s::%s() but misses parameter #%d $%s.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - $i + 1, - $prototypeParameter->getName() - ))->nonIgnorable()->build(); - continue; - } - - $methodParameter = $methodParameters[$i]; - if ($prototypeParameter->passedByReference()->no()) { - if (!$methodParameter->passedByReference()->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is passed by reference but parameter #%d $%s of method %s::%s() is not passed by reference.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - } - } elseif ($methodParameter->passedByReference()->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not passed by reference but parameter #%d $%s of method %s::%s() is passed by reference.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - } - - if ($prototypeParameter->isVariadic()) { - $prototypeAfterVariadic = true; - if (!$methodParameter->isVariadic()) { - if (!$methodParameter->isOptional()) { - if (count($methodParameters) !== $i + 1) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not optional.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->nonIgnorable()->build(); - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic but parameter #%d $%s of method %s::%s() is variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - continue; - } elseif (count($methodParameters) === $i + 1) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->nonIgnorable()->build(); - } - } - } elseif ($methodParameter->isVariadic()) { - if ($this->phpVersion->supportsLessOverridenParametersWithVariadic()) { - $remainingPrototypeParameters = array_slice($prototypeVariant->getParameters(), $i); - foreach ($remainingPrototypeParameters as $j => $remainingPrototypeParameter) { - if (!$remainingPrototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - if ($methodParameter->getNativeType()->isSuperTypeOf($remainingPrototypeParameter->getNativeType())->yes()) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d ...$%s (%s) of method %s::%s() is not contravariant with parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + $j + 1, - $remainingPrototypeParameter->getName(), - $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - } - break; - } - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is variadic but parameter #%d $%s of method %s::%s() is not variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - continue; - } - - if ($prototypeParameter->isOptional() && !$methodParameter->isOptional()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is required but parameter #%d $%s of method %s::%s() is optional.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - } - - $methodParameterType = $methodParameter->getNativeType(); - - if (!$prototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - - $prototypeParameterType = $prototypeParameter->getNativeType(); - if (!$this->phpVersion->supportsParameterTypeWidening()) { - if (!$methodParameterType->equals($prototypeParameterType)) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s (%s) of method %s::%s() does not match parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameterType->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); - } - continue; - } + $methodReturnType = $method->getNativeReturnType(); - if ($this->isTypeCompatible($methodParameterType, $prototypeParameterType, $this->phpVersion->supportsParameterContravariance())) { - continue; - } + $realPrototype = $method->getPrototype(); - 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 ( - $j === count($methodParameters) - 1 - && $prototypeAfterVariadic - && !$methodParameter->isVariadic() - ) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic.', - $j + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->nonIgnorable()->build(); - continue; - } - - if (!$methodParameter->isOptional()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not optional.', - $j + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->nonIgnorable()->build(); - continue; + $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(); } } - $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 (!$this->isTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance())) { + if ( + $reportReturnType + && (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + ) { + $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); + } + } + } + + if ( + $reportReturnType + && !$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance()) + ) { if ($this->phpVersion->supportsReturnCovariance()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with return type %s of method %s::%s().', @@ -396,9 +267,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().', @@ -406,53 +280,50 @@ 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 Node\AttributeGroup[] $attrGroups + * @return Node\AttributeGroup[] + */ + private function filterOverrideAttribute(array $attrGroups): array { - if ($methodParameterType instanceof MixedType) { - return true; - } - - if (!$supportsContravariance) { - if (TypeCombinator::containsNull($methodParameterType)) { - $prototypeParameterType = TypeCombinator::removeNull($prototypeParameterType); - } - $methodParameterType = TypeCombinator::removeNull($methodParameterType); - if ($methodParameterType->equals($prototypeParameterType)) { - return true; - } - - if ($methodParameterType instanceof IterableType) { - if ($prototypeParameterType instanceof ArrayType) { - return true; + foreach ($attrGroups as $i => $attrGroup) { + foreach ($attrGroup->attrs as $j => $attr) { + if ($attr->name->toLowerString() !== 'override') { + continue; } - if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === \Traversable::class) { - return true; + + unset($attrGroup->attrs[$j]); + if (count($attrGroup->attrs) !== 0) { + continue; } - } - return false; + unset($attrGroups[$i]); + } } - return $methodParameterType->isSuperTypeOf($prototypeParameterType)->yes(); + return $attrGroups; } /** - * @param RuleError[] $errors - * @return (string|RuleError)[] + * @param list $errors + * @return list */ private function addErrors( array $errors, InClassMethodNode $classMethod, - Scope $scope + Scope $scope, ): array { if (count($errors) > 0) { @@ -466,4 +337,30 @@ private function addErrors( return $this->methodSignatureRule->processNode($classMethod, $scope); } + private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'returntypewillchange') { + return true; + } + } + } + + return false; + } + + 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; + } + } diff --git a/src/Rules/Methods/ReturnTypeRule.php b/src/Rules/Methods/ReturnTypeRule.php index 451eb96a26..9c9882fedf 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -5,21 +5,34 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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,42 +51,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( - 'Method %s::%s() should never return but return statement found.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() + '%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..b0a6f057e5 --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -0,0 +1,326 @@ +, 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..e1b33d1321 --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -0,0 +1,68 @@ + + */ +#[RegisteredRule(level: 0)] +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/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index ed69a725b0..5bc9d3d40e 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -2,39 +2,41 @@ namespace PHPStan\Rules\Missing; +use Generator; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ExecutionEndNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\GenericTypeVariableResolver; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ExecutionEndNode> + * @implements Rule */ -class MissingReturnRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingReturnRule implements Rule { - private bool $checkExplicitMixedMissingReturn; - - private bool $checkPhpDocMissingReturn; - public function __construct( - bool $checkExplicitMixedMissingReturn, - bool $checkPhpDocMissingReturn + #[AutowiredParameter] + private bool $checkExplicitMixedMissingReturn, + #[AutowiredParameter] + private bool $checkPhpDocMissingReturn, ) { - $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; - $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; } public function getNodeType(): string @@ -58,38 +60,43 @@ public function processNode(Node $node, Scope $scope): array return []; } } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); - if ($scopeFunction instanceof MethodReflection) { - $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + $returnType = $scopeFunction->getReturnType(); + if ($scopeFunction instanceof PhpMethodFromParserNodeReflection) { + if (!$scopeFunction->isPropertyHook()) { + $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + } else { + $description = sprintf('%s hook for property %s::$%s', ucfirst($scopeFunction->getPropertyHookName()), $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getHookedPropertyName()); + } } else { $description = sprintf('Function %s()', $scopeFunction->getName()); } } 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(), ]; } } @@ -112,6 +119,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.never'); + return [ $errorBuilder->build(), ]; @@ -130,13 +139,15 @@ public function processNode(Node $node, Scope $scope): array } $errorBuilder = RuleErrorBuilder::message( - sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())) + sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), )->line($node->getNode()->getStartLine()); if ($node->hasNativeReturnTypehint()) { $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.missing'); + return [ $errorBuilder->build(), ]; diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 9d8ac31dd9..e92904cd09 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -2,10 +2,20 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ReflectionProvider; +use Closure; +use Generator; +use Iterator; +use IteratorAggregate; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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; @@ -13,61 +23,46 @@ 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; + +#[AutowiredService] +final class MissingTypehintCheck { - public const TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; - - public const TURN_OFF_NON_GENERIC_CHECK_TIP = 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.'; + public const MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; private const ITERABLE_GENERIC_CLASS_NAMES = [ - \Traversable::class, - \Iterator::class, - \IteratorAggregate::class, - \Generator::class, + Traversable::class, + Iterator::class, + IteratorAggregate::class, + Generator::class, ]; - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkMissingIterableValueType; - - private bool $checkGenericClassInNonGenericObjectType; - - private bool $checkMissingCallableSignature; - - /** @var string[] */ - private array $skipCheckGenericClasses; - /** * @param string[] $skipCheckGenericClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkMissingIterableValueType, - bool $checkGenericClassInNonGenericObjectType, - bool $checkMissingCallableSignature, - array $skipCheckGenericClasses = [] + #[AutowiredParameter] + private bool $checkMissingCallableSignature, + #[AutowiredParameter(ref: '%featureToggles.skipCheckGenericClasses%')] + private array $skipCheckGenericClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkMissingIterableValueType = $checkMissingIterableValueType; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; - $this->checkMissingCallableSignature = $checkMissingCallableSignature; - $this->skipCheckGenericClasses = $skipCheckGenericClasses; } /** - * @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) { @@ -76,26 +71,27 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($type instanceof AccessoryType) { return $type; } + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $iterablesWithMissingValueTypehint = array_merge( + $iterablesWithMissingValueTypehint, + $this->getIterableTypesWithMissingValueTypehint($type->getIf()), + $this->getIterableTypesWithMissingValueTypehint($type->getElse()), + ); + + return $type; + } if ($type->isIterable()->yes()) { $iterableValue = $type->getIterableValueType(); if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) { - if ( - $type instanceof TypeWithClassName - && !in_array($type->getClassName(), self::ITERABLE_GENERIC_CLASS_NAMES, true) - && $this->reflectionProvider->hasClass($type->getClassName()) - ) { - $classReflection = $this->reflectionProvider->getClass($type->getClassName()); - if ($classReflection->isGeneric()) { - return $type; - } - } $iterablesWithMissingValueTypehint[] = $type; } - if (!$type instanceof IntersectionType) { - return $traverse($type); - } + if ($type instanceof IntersectionType) { + if ($type->isList()->yes()) { + return $traverse($iterableValue); + } - return $type; + return $type; + } } return $traverse($type); }); @@ -104,18 +100,13 @@ 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, function (Type $type, callable $traverse) use (&$objectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $traverse($type); return $type; } @@ -143,11 +134,24 @@ 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; } @@ -159,8 +163,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getCallablesWithMissingSignature(Type $type): array { @@ -171,8 +174,10 @@ public function getCallablesWithMissingSignature(Type $type): array $result = []; TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( - ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === \Closure::class)) { + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php new file mode 100644 index 0000000000..61dd8f285c --- /dev/null +++ b/src/Rules/Names/UsedNamesRule.php @@ -0,0 +1,151 @@ + + */ +#[RegisteredRule(level: 0)] +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 b6854978f2..6885c42817 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -5,38 +5,40 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Use_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + 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 +48,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 +60,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 +75,35 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function checkConstant(Node\Name $name): ?RuleError + private function checkConstant(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasConstant($name, null)) { - return RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name)) + ->line($name->getStartLine()) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } return null; } - private function checkFunction(Node\Name $name): ?RuleError + private function checkFunction(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasFunction($name, null)) { - return RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name)) + ->line($name->getStartLine()) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } if ($this->checkFunctionNameCase) { @@ -99,26 +117,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 40a575934b..73490e22c5 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -4,49 +4,51 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + 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 +60,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 +75,38 @@ private function checkConstants(array $uses): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name))->line($use->name->getLine())->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } return $errors; } /** - * @param \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())->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } elseif ($this->checkFunctionNameCase) { $functionReflection = $this->reflectionProvider->getFunction($use->name, null); $realName = $functionReflection->getName(); @@ -100,8 +118,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 +131,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 index fd17b1f850..4bd8d724ad 100644 --- a/src/Rules/NullsafeCheck.php +++ b/src/Rules/NullsafeCheck.php @@ -3,8 +3,10 @@ namespace PHPStan\Rules; use PhpParser\Node\Expr; +use PHPStan\DependencyInjection\AutowiredService; -class NullsafeCheck +#[AutowiredService] +final class NullsafeCheck { public function containsNullSafe(Expr $expr): bool @@ -36,7 +38,7 @@ public function containsNullSafe(Expr $expr): bool return $this->containsNullSafe($expr->class); } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { foreach ($expr->items as $item) { if ($item === null) { continue; diff --git a/src/Rules/Operators/InvalidAssignVarRule.php b/src/Rules/Operators/InvalidAssignVarRule.php index 2c700f4e81..fd56b64107 100644 --- a/src/Rules/Operators/InvalidAssignVarRule.php +++ b/src/Rules/Operators/InvalidAssignVarRule.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\AssignOp; use PhpParser\Node\Expr\AssignRef; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,14 +16,12 @@ /** * @implements Rule */ -class InvalidAssignVarRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidAssignVarRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string @@ -42,26 +41,34 @@ public function processNode(Node $node, Scope $scope): array if ($this->nullsafeCheck->containsNullSafe($node->var)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.') + ->identifier('nullsafe.assign') + ->nonIgnorable() + ->build(), ]; } if ($node instanceof AssignRef && $this->nullsafeCheck->containsNullSafe($node->expr)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.') + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(), ]; } if ($this->containsNonAssignableExpression($node->var)) { return [ - RuleErrorBuilder::message('Expression on left side of assignment is not assignable.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expression on left side of assignment is not assignable.') + ->identifier('assign.invalidExpr') + ->nonIgnorable() + ->build(), ]; } return []; } - private function containsNonAssignableExpression(Expr $expr): bool { if ($expr instanceof Expr\Variable) { @@ -80,7 +87,7 @@ private function containsNonAssignableExpression(Expr $expr): bool return false; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { foreach ($expr->items as $item) { if ($item === null) { continue; diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 9cabaec6be..374aa87bfc 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -5,29 +5,32 @@ use PhpParser\Node; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 2)] +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 +38,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 +47,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..b410ae0fb9 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -4,27 +4,41 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ArrayType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectWithoutClassType; 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\BinaryOp> + * @implements Rule */ -class InvalidComparisonOperationRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 2)] +final class InvalidComparisonOperationRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + #[AutowiredParameter(ref: '%featureToggles.checkExtensionsForComparisonOperators%')] + private bool $checkExtensionsForComparisonOperators, + ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -46,22 +60,37 @@ public function processNode(Node $node, Scope $scope): array return []; } + $isLeftNumberType = $this->isNumberType($scope, $node->left); + $isRightNumberType = $this->isNumberType($scope, $node->right); + if ($isLeftNumberType === $isRightNumberType) { + return []; + } + + $result = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->callOperatorTypeSpecifyingExtensions( + $node, + $scope->getType($node->left), + $scope->getType($node->right), + ); + + if ($result !== null) { + if (! $result instanceof ErrorType) { + return []; + } + + if ($this->checkExtensionsForComparisonOperators) { + return $this->createError($node, $scope); + } + } + if ( - ($this->isNumberType($scope, $node->left) && ( - $this->isObjectType($scope, $node->right) || $this->isArrayType($scope, $node->right) + ($isLeftNumberType && ( + $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) + || ($isRightNumberType && ( + $this->isPossiblyNullableObjectType($scope, $node->left) || $this->isPossiblyNullableArrayType($scope, $node->left) )) ) { - 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(), - ]; + return $this->createError($node, $scope); } return []; @@ -70,9 +99,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,46 +110,66 @@ 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(); - - $type = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $expr, - '', - static function (Type $type) use ($acceptedType): bool { - return $acceptedType->isSuperTypeOf($type)->yes(); - } - )->getType(); + $type = $scope->getType($expr); + $acceptedType = new UnionType([new ObjectWithoutClassType(), new NullType()]); - if ($type instanceof ErrorType) { - return false; - } + return !$type->isNull()->yes() && $acceptedType->isSuperTypeOf($type)->yes(); + } - $isSuperType = $acceptedType->isSuperTypeOf($type); - if ($type instanceof \PHPStan\Type\BenevolentUnionType) { - return !$isSuperType->no(); - } + private function isPossiblyNullableArrayType(Scope $scope, Node\Expr $expr): bool + { + $type = $scope->getType($expr); + $acceptedType = new UnionType([new ArrayType(new MixedType(), new MixedType()), new NullType()]); - return $isSuperType->yes(); + return !$type->isNull()->yes() && $acceptedType->isSuperTypeOf($type)->yes(); } - private function isArrayType(Scope $scope, Node\Expr $expr): bool + /** @return list */ + private function createError(Node\Expr\BinaryOp $node, Scope $scope): array { - $type = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $expr, - '', - static function (Type $type): bool { - return $type->isArray()->yes(); - } - )->getType(); + 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 !($type instanceof ErrorType) && $type->isArray()->yes(); + 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->getStartLine()) + ->identifier(sprintf('%s.invalid', $nodeType)) + ->build(), + ]; } } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 1bbc260fa5..c52a790ef6 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -2,74 +2,113 @@ namespace PHPStan\Rules\Operators; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +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 feddcd33be..1b688dc06d 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -2,51 +2,96 @@ namespace PHPStan\Rules\Operators; +use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 2)] +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 \PhpParser\Node\Expr\BitwiseNot + !$node instanceof Node\Expr\UnaryPlus + && !$node instanceof Node\Expr\UnaryMinus + && !$node instanceof Node\Expr\BitwiseNot ) { return []; } - if ($scope->getType($node) instanceof ErrorType) { - - if ($node instanceof \PhpParser\Node\Expr\UnaryPlus) { - $operator = '+'; - } elseif ($node instanceof \PhpParser\Node\Expr\UnaryMinus) { - $operator = '-'; - } else { - $operator = '~'; - } - return [ - RuleErrorBuilder::message(sprintf( - 'Unary operation "%s" on %s results in an error.', - $operator, - $scope->getType($node->expr)->describe(VerbosityLevel::value()) - ))->line($node->expr->getLine())->build(), - ]; + $varName = '__PHPSTAN__LEFT__'; + $variable = new Node\Expr\Variable($varName); + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $newNode->expr = $variable; + + if ($node instanceof Node\Expr\BitwiseNot) { + $callback = static fn (Type $type): bool => $type->isString()->yes() || $type->isInteger()->yes() || $type->isFloat()->yes(); + } else { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; + } + + $exprType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + '', + $callback, + )->getType(); + if ($exprType instanceof ErrorType) { + return []; } - return []; + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $scope = $scope->assignVariable($varName, $exprType, $exprType, TrinaryLogic::createYes()); + if (!$scope->getType($newNode) instanceof ErrorType) { + return []; + } + + if ($node instanceof Node\Expr\UnaryPlus) { + $operator = '+'; + } elseif ($node instanceof Node\Expr\UnaryMinus) { + $operator = '-'; + } else { + $operator = '~'; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Unary operation "%s" on %s results in an error.', + $operator, + $scope->getType($node->expr)->describe(VerbosityLevel::value()), + )) + ->line($node->expr->getStartLine()) + ->identifier('unaryOp.invalid') + ->build(), + ]; } } diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php new file mode 100644 index 0000000000..244cd1ba4f --- /dev/null +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -0,0 +1,74 @@ +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..0dc14b638a --- /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(), classReflection: $class); + } + + $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 = $scope->getType($assertedExpr); + $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..af7cfda155 --- /dev/null +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -0,0 +1,130 @@ + + */ + 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..46eba83a07 --- /dev/null +++ b/src/Rules/PhpDoc/FunctionAssertRule.php @@ -0,0 +1,39 @@ + + */ +#[RegisteredRule(level: 2)] +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..b5a1a713bf --- /dev/null +++ b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php @@ -0,0 +1,39 @@ + + */ +#[RegisteredRule(level: 2)] +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..d38279a659 --- /dev/null +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -0,0 +1,122 @@ + $functionTemplateTags + * + * @return list + */ + public function check( + Node $node, + Scope $scope, + string $location, + Type $callableType, + ?string $functionName, + array $functionTemplateTags, + ?ClassReflection $classReflection, + ): array + { + $errors = []; + + TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) { + if (!($type instanceof CallableType || $type instanceof ClosureType)) { + return $traverse($type); + } + + $typeDescription = $type->describe(VerbosityLevel::precise()); + + $errors = $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithAnonymousFunction(), + $type->getTemplateTags(), + sprintf('PHPDoc tag %s template of %s cannot have existing class %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid default type %%s.', $location, $typeDescription), + sprintf('Default type %%s in PHPDoc tag %s template %%s of %s is not subtype of bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s does not have a default type but follows an optional template %%s.', $location, $typeDescription), + ); + + $templateTags = $type->getTemplateTags(); + + $classDescription = null; + if ($classReflection !== null) { + $classDescription = $classReflection->getDisplayName(); + } + + if ($functionName !== null) { + $functionDescription = sprintf('function %s', $functionName); + if ($classReflection !== null) { + $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); + } + + foreach (array_keys($functionTemplateTags) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', + $location, + $name, + $typeDescription, + $name, + $functionDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + if ($classReflection !== null) { + foreach (array_keys($classReflection->getTemplateTags()) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for class %s.', + $location, + $name, + $typeDescription, + $name, + $classDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + return $traverse($type); + }); + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index 83e97fa8a3..5d946e4488 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -4,33 +4,32 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ConstantTypeHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class IncompatibleClassConstantPhpDocTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleClassConstantPhpDocTypeRule implements Rule { - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - GenericObjectTypeCheck $genericObjectTypeCheck, - UnresolvableTypeHelper $unresolvableTypeHelper + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, ) { - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } public function getNodeType(): string @@ -41,35 +40,34 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); } $errors = []; foreach ($node->consts as $const) { $constantName = $const->name->toString(); - $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $nativeType, $constantName)); } return $errors; } /** - * @param string $constantName - * @return RuleError[] + * @return list */ - private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { return []; } - if (!$constantReflection->hasPhpDocType()) { - return []; - } - - $phpDocType = $constantReflection->getValueType(); - $errors = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -77,29 +75,27 @@ private function processSingleConstant(ClassReflection $classReflection, string $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @var for constant %s::%s contains unresolvable type.', $constantReflection->getDeclaringClass()->getName(), - $constantName - ))->build(); - } else { - $nativeType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); - $isSuperType = $phpDocType->isSuperTypeOf($nativeType); - $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType); + $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 value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()) - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } elseif ($isSuperType->maybe()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()) - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } } @@ -109,25 +105,35 @@ private function processSingleConstant(ClassReflection $classReflection, string return array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( - 'PHPDoc tag @var for constant %s::%s contains generic type %%s but class %%s is not generic.', + 'PHPDoc tag @var for constant %s::%s contains generic type %%s but %%s %%s is not generic.', $className, - $escapedConstantName + $escapedConstantName, ), sprintf( - 'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of class %%s: %%s', + 'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of %%s %%s: %%s', $className, - $escapedConstantName + $escapedConstantName, ), sprintf( - 'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but class %%s supports only %%d: %%s', + 'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $className, - $escapedConstantName + $escapedConstantName, ), sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of class %%s.', + '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 - ) + $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..702f82628f --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php @@ -0,0 +1,96 @@ + + */ +#[RegisteredRule(level: 2)] +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..8fcc1569d2 --- /dev/null +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php @@ -0,0 +1,238 @@ + $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 5da471cdb6..e5a82505fc 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,51 +5,35 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; -use PHPStan\Internal\SprintfHelper; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ArrayType; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateTypeHelper; 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 +#[RegisteredRule(level: 2)] +final class IncompatiblePhpDocTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericObjectTypeCheck $genericObjectTypeCheck, - UnresolvableTypeHelper $unresolvableTypeHelper + private FileTypeMapper $fileTypeMapper, + private IncompatiblePhpDocTypeCheck $check, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } public function getNodeType(): string { - return \PhpParser\Node\FunctionLike::class; + return Node\FunctionLike::class; } public function processNode(Node $node, Scope $scope): array { - $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_) { @@ -58,147 +42,68 @@ public function processNode(Node $node, Scope $scope): array return []; } + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $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 ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s contains unresolvable type.', - $parameterName - ))->build(); - - } else { - $nativeParamType = $nativeParameterTypes[$parameterName]; - if ( - $phpDocParamTag->isVariadic() - && $phpDocParamType instanceof ArrayType - && !$nativeParamType instanceof ArrayType - ) { - $phpDocParamType = $phpDocParamType->getItemType(); - } - $isParamSuperType = $nativeParamType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocParamType)); - $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocParamType, - sprintf( - 'PHPDoc tag @param for parameter $%s contains generic type %%s but class %%s is not generic.', - $escapedParameterName - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of class %%s: %%s', - $escapedParameterName - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but class %%s supports only %%d: %%s', - $escapedParameterName - ), - sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of class %%s.', - $escapedParameterName - ) - )); - - if ($isParamSuperType->no()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()) - ))->build(); - - } elseif ($isParamSuperType->maybe()) { - $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 ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) - ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->build(); - - } else { - $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocReturnType, - 'PHPDoc tag @return contains generic type %s but 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..f786abba9d --- /dev/null +++ b/src/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRule.php @@ -0,0 +1,87 @@ + + */ +#[RegisteredRule(level: 2)] +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 0f2ab5119d..6ebc2e639c 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -4,30 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -class IncompatiblePropertyPhpDocTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatiblePropertyPhpDocTypeRule implements Rule { - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - GenericObjectTypeCheck $genericObjectTypeCheck, - UnresolvableTypeHelper $unresolvableTypeHelper + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } public function getNodeType(): string @@ -37,23 +37,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + $phpDocType = $node->getPhpDocType(); + if ($phpDocType === null) { + return []; } $propertyName = $node->getName(); - $propertyReflection = $scope->getClassReflection()->getNativeProperty($propertyName); - if (!$propertyReflection->hasPhpDocType()) { - return []; - } - - $phpDocType = $propertyReflection->getPhpDocType(); $description = 'PHPDoc tag @var'; - if ($propertyReflection->isPromoted()) { + if ($node->isPromoted()) { $description = 'PHPDoc type'; } + $classReflection = $node->getClassReflection(); + $messages = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -61,63 +58,95 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s contains unresolvable type.', $description, - $propertyReflection->getDeclaringClass()->getName(), - $propertyName - ))->build(); - } - - $nativeType = $propertyReflection->getNativeType(); - $isSuperType = $nativeType->isSuperTypeOf($phpDocType); - if ($isSuperType->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s for property %s::$%s with type %s is incompatible with native type %s.', - $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), + $classReflection->getDisplayName(), $propertyName, - $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + ))->identifier('property.unresolvableType')->build(); + } - } elseif ($isSuperType->maybe()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s for property %s::$%s with type %s is not subtype of native type %s.', - $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName, - $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $nativeType = $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(); + } } - $className = SprintfHelper::escapeFormatString($propertyReflection->getDeclaringClass()->getDisplayName()); + $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + if ($node->isPromoted() === false) { + $messages = array_merge($messages, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@var', + $phpDocType, + null, + [], + $classReflection, + )); + } + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( - '%s for property %s::$%s contains generic type %%s but class %%s is not generic.', + '%s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $description, $className, - $escapedPropertyName + $escapedPropertyName, ), sprintf( - 'Generic type %%s in %s for property %s::$%s does not specify all template types of class %%s: %%s', + 'Generic type %%s in %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $description, $className, - $escapedPropertyName + $escapedPropertyName, ), sprintf( - 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s', + 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $description, $className, - $escapedPropertyName + $escapedPropertyName, ), sprintf( - 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of class %%s.', + 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $description, $className, - $escapedPropertyName - ) + $escapedPropertyName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', + $description, + $className, + $escapedPropertyName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', + $description, + $className, + $escapedPropertyName, + ), )); return $messages; diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php new file mode 100644 index 0000000000..9efce61b95 --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -0,0 +1,105 @@ + + */ +#[RegisteredRule(level: 2)] +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(), classReflection: $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 f17d2df84e..d909dfab72 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -3,65 +3,93 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 2)] +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-sealed', + '@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) { @@ -73,7 +101,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; @@ -81,8 +109,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 664437b289..4b18ca789a 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -3,47 +3,54 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 2)] +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 - && !$node instanceof Node\Stmt\ClassConst - ) { + // mirrored with InvalidPHPStanDocTagRule + if ($node instanceof VirtualNode) { return []; } + if (!$node instanceof Node\Stmt && !$node instanceof Node\PropertyHook) { + return []; + } + if ($node instanceof Node\Stmt\Expression) { + if (!$node->expr instanceof Node\Expr\Assign && !$node->expr instanceof Node\Expr\AssignRef) { + return []; + } + } $docComment = $node->getDocComment(); if ($docComment === null) { @@ -56,11 +63,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; } @@ -68,8 +90,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 c5d0c35576..eadf89f84b 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -4,71 +4,57 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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\FileTypeMapper; 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\Stmt> + * @implements Rule */ -class InvalidPhpDocVarTagTypeRule implements Rule +#[RegisteredRule(level: 2)] +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 UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkClassCaseSensitivity; - - private bool $checkMissingVarTagTypehint; - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkClassCaseSensitivity, - bool $checkMissingVarTagTypehint + private FileTypeMapper $fileTypeMapper, + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private MissingTypehintCheck $missingTypehintCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter] + private bool $checkMissingVarTagTypehint, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkMissingVarTagTypehint = $checkMissingVarTagTypehint; } public function getNodeType(): string { - return \PhpParser\Node\Stmt::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Stmt\Property - || $node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Stmt\ClassConst || $node instanceof Node\Stmt\Const_ ) { @@ -86,7 +72,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 = []; @@ -99,7 +85,10 @@ public function processNode(Node $node, Scope $scope): array if ( $this->unresolvableTypeHelper->containsUnresolvableType($varTagType) ) { - $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier))->line($docComment->getStartLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier)) + ->line($docComment->getStartLine()) + ->identifier('varTag.unresolvableType') + ->build(); continue; } @@ -109,56 +98,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(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->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.', $escapedIdentifier), - sprintf('Generic type %%s in %s does not specify all template types of class %%s: %%s', $escapedIdentifier), - sprintf('Generic type %%s in %s specifies %%d template types, but class %%s supports only %%d: %%s', $escapedIdentifier), - sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of class %%s.', $escapedIdentifier) + 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 - ))->discoveringSymbolsTip()->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 8027a757f0..9af949a91f 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -3,40 +3,50 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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\Stmt> + * @implements Rule */ -class InvalidThrowsPhpDocValueRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 2)] +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\Stmt::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { + return []; // is handled by virtual nodes + } + } elseif (!$node instanceof InPropertyHookNode) { return []; } - if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { - return []; // is handled by virtual nodes + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; } $functionName = null; @@ -49,7 +59,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) { @@ -57,21 +67,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..fbfcf14eaf --- /dev/null +++ b/src/Rules/PhpDoc/MethodAssertRule.php @@ -0,0 +1,39 @@ + + */ +#[RegisteredRule(level: 2)] +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..7ef6b6e2f3 --- /dev/null +++ b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php @@ -0,0 +1,39 @@ + + */ +#[RegisteredRule(level: 2)] +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..6323fb6a83 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -0,0 +1,106 @@ + $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..5c56c41d0a --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -0,0 +1,52 @@ + + */ +#[RegisteredRule(level: 2)] +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..6f69533d53 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php @@ -0,0 +1,45 @@ + + */ +#[RegisteredRule(level: 2)] +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..f012a0dfff --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -0,0 +1,42 @@ + + */ +#[RegisteredRule(level: 2)] +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..f27d022dd2 --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -0,0 +1,103 @@ + + */ +#[RegisteredRule(level: 2)] +final class RequireImplementsDefinitionTraitRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + 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/SealedDefinitionClassRule.php b/src/Rules/PhpDoc/SealedDefinitionClassRule.php new file mode 100644 index 0000000000..3ef882d2ae --- /dev/null +++ b/src/Rules/PhpDoc/SealedDefinitionClassRule.php @@ -0,0 +1,100 @@ + + */ +#[RegisteredRule(level: 2)] +final class SealedDefinitionClassRule implements Rule +{ + + public function __construct( + private ClassNameCheck $classCheck, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $sealedTags = $classReflection->getSealedTags(); + + if (count($sealedTags) === 0) { + return []; + } + + if ($classReflection->isEnum()) { + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-sealed is only valid on class or interface.') + ->identifier('sealed.onEnum') + ->build(), + ]; + } + + $errors = []; + foreach ($sealedTags as $sealedTag) { + $type = $sealedTag->getType(); + $classNames = $type->getObjectClassNames(); + if (count($classNames) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-sealed contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('sealed.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-sealed contains unknown class %s.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + continue; + } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_SEALED), $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/SealedDefinitionTraitRule.php b/src/Rules/PhpDoc/SealedDefinitionTraitRule.php new file mode 100644 index 0000000000..7a0c9bafd6 --- /dev/null +++ b/src/Rules/PhpDoc/SealedDefinitionTraitRule.php @@ -0,0 +1,54 @@ + + */ +#[RegisteredRule(level: 0)] +final class SealedDefinitionTraitRule 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 + || !$this->reflectionProvider->hasClass($node->namespacedName->toString()) + ) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + $sealedTags = $traitReflection->getSealedTags(); + + if (count($sealedTags) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-sealed is only valid on class or interface.') + ->identifier('sealed.onTrait') + ->build(), + ]; + } + +} diff --git a/src/Rules/PhpDoc/UnresolvableTypeHelper.php b/src/Rules/PhpDoc/UnresolvableTypeHelper.php index 8b53af21d5..69f539f373 100644 --- a/src/Rules/PhpDoc/UnresolvableTypeHelper.php +++ b/src/Rules/PhpDoc/UnresolvableTypeHelper.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -class UnresolvableTypeHelper +#[AutowiredService] +final class UnresolvableTypeHelper { public function containsUnresolvableType(Type $type): bool diff --git a/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php new file mode 100644 index 0000000000..6632d6320b --- /dev/null +++ b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php @@ -0,0 +1,32 @@ + + */ +#[RegisteredRule(level: 2)] +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..c8c9c212b8 --- /dev/null +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -0,0 +1,258 @@ + + */ + 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 dee13b4357..a517f4e38e 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -6,39 +6,52 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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\Stmt> + * @implements Rule */ -class WrongVariableNameInVarTagRule implements Rule +#[RegisteredRule(level: 2)] +final class WrongVariableNameInVarTagRule implements Rule { - private FileTypeMapper $fileTypeMapper; - public function __construct( - FileTypeMapper $fileTypeMapper + private FileTypeMapper $fileTypeMapper, + private VarTagTypeRuleHelper $varTagTypeRuleHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; } public function getNodeType(): string { - return \PhpParser\Node\Stmt::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Stmt\Property - || $node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Stmt\ClassConst || $node instanceof Node\Stmt\Const_ || ($node instanceof VirtualNode && !$node instanceof InFunctionNode && !$node instanceof InClassMethodNode && !$node instanceof InClassNode) @@ -57,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { $varTags[$key] = $varTag; @@ -69,18 +82,21 @@ public function processNode(Node $node, Scope $scope): array } if ($node instanceof Node\Stmt\Foreach_) { - return $this->processForeach($node->expr, $node->keyVar, $node->valueVar, $varTags); + return $this->processForeach($scope, $node->expr, $node->keyVar, $node->valueVar, $varTags); } if ($node instanceof Node\Stmt\Static_) { - return $this->processStatic($node->vars, $varTags); + return $this->processStatic($scope, $node->vars, $varTags); } if ($node instanceof Node\Stmt\Expression) { + if ($node->expr instanceof Expr\Throw_) { + return $this->processStmt($scope, $varTags, $node->expr); + } return $this->processExpression($scope, $node->expr, $varTags); } - if ($node instanceof Node\Stmt\Throw_ || $node instanceof Node\Stmt\Return_) { + if ($node instanceof Node\Stmt\Return_) { return $this->processStmt($scope, $varTags, $node->expr); } @@ -95,8 +111,10 @@ public function processNode(Node $node, Scope $scope): array $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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } elseif ($originalNode instanceof Node\Stmt\ClassMethod) { $description = 'a method'; } @@ -104,8 +122,8 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'PHPDoc tag @var above %s has no effect.', - $description - ))->build(), + $description, + ))->identifier('varTag.misplaced')->build(), ]; } @@ -113,12 +131,10 @@ public function processNode(Node $node, Scope $scope): array } /** - * @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 { $errors = []; $hasMultipleMessage = false; @@ -127,13 +143,15 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar if (is_int($key)) { if (count($varTags) !== 1) { if (!$hasMultipleMessage) { - $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.')->build(); + $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.') + ->identifier('varTag.multipleTags') + ->build(); $hasMultipleMessage = true; } } elseif (count($assignedVariables) !== 1) { $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above assignment does not specify variable name.' - )->build(); + 'PHPDoc tag @var above assignment does not specify variable name.', + )->identifier('varTag.noVariable')->build(); } continue; } @@ -150,10 +168,18 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', $key, - $assignedVariables[0] - ))->build(); + $assignedVariables[0], + ))->identifier('varTag.differentVariable')->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key)) + ->identifier('varTag.variableNotFound') + ->build(); + } + } + + if (count($errors) === 0) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { + $errors[] = $error; } } @@ -161,7 +187,6 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar } /** - * @param Expr $expr * @return string[] */ private function getAssignedVariables(Expr $expr): array @@ -174,7 +199,7 @@ private function getAssignedVariables(Expr $expr): array return []; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { $names = []; foreach ($expr->items as $item) { if ($item === null) { @@ -191,12 +216,10 @@ private function getAssignedVariables(Expr $expr): array } /** - * @param \PhpParser\Node\Expr|null $keyVar - * @param \PhpParser\Node\Expr $valueVar - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return list */ - private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array + private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array { $variableNames = []; if ($iterateeExpr instanceof Node\Expr\Variable && is_string($iterateeExpr->name)) { @@ -214,8 +237,8 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod continue; } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above foreach loop does not specify variable name.' - )->build(); + 'PHPDoc tag @var above foreach loop does not specify variable name.', + )->identifier('varTag.noVariable')->build(); continue; } @@ -226,21 +249,44 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any variable in the foreach loop: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, $variableNames)) - ))->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 \PhpParser\Node\Stmt\StaticVar[] $vars - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return list + */ + private function processExpression(Scope $scope, Expr $expr, array $varTags): array + { + if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { + return $this->processAssign($scope, $expr->var, $expr->expr, $varTags); + } + + return $this->processStmt($scope, $varTags, null); + } + + /** + * @param Node\Stmt\StaticVar[] $vars + * @param VarTag[] $varTags + * @return list */ - private function processStatic(array $vars, array $varTags): array + private function processStatic(Scope $scope, array $vars, array $varTags): array { $variableNames = []; foreach ($vars as $var) { @@ -248,7 +294,7 @@ private function processStatic(array $vars, array $varTags): array continue; } - $variableNames[$var->var->name] = true; + $variableNames[] = $var->var->name; } $errors = []; @@ -259,47 +305,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 $this->processAssign($scope, $expr->var, $varTags); + foreach ($vars as $var) { + if ($var->default === null) { + continue; + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var->var, $var->default, $varTags, $variableNames) as $error) { + $errors[] = $error; + } } - return $this->processStmt($scope, $varTags, null); + return $errors; } /** - * @param \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 { @@ -316,12 +352,16 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name)) + ->identifier('varTag.variableNotFound') + ->build(); } if (count($variableLessVarTags) !== 1 || $defaultExpr === null) { if (count($variableLessVarTags) > 0) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.')->build(); + $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.') + ->identifier('varTag.noVariable') + ->build(); } } @@ -329,9 +369,8 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return list */ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $varTags): array { @@ -355,8 +394,8 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above multiple global variables does not specify variable name.' - )->build(); + 'PHPDoc tag @var above multiple global variables does not specify variable name.', + )->identifier('varTag.noVariable')->build(); continue; } @@ -367,10 +406,8 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any global variable: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, array_keys($variableNames))) - ))->build(); + 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..a0870f7ce3 --- /dev/null +++ b/src/Rules/Playground/NeverRuleHelper.php @@ -0,0 +1,51 @@ +|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/PhpdocCommentRule.php b/src/Rules/Playground/PhpdocCommentRule.php new file mode 100644 index 0000000000..f5588049b2 --- /dev/null +++ b/src/Rules/Playground/PhpdocCommentRule.php @@ -0,0 +1,52 @@ + + */ +final class PhpdocCommentRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof VirtualNode) { + return []; + } + + $comments = $node->getComments(); + + $errors = []; + foreach ($comments as $comment) { + foreach (['/**', '//', '#'] as $startTag) { + if (str_starts_with($comment->getText(), $startTag)) { + continue 2; + } + } + + if (Strings::match($comment->getText(), '{(\s|^)@\w+(\s|$)}') === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message('Comment contains PHPDoc tag but does not start with /** prefix.') + ->identifier('phpstanPlayground.phpDoc') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php new file mode 100644 index 0000000000..9381a36922 --- /dev/null +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -0,0 +1,125 @@ + + */ +final class PromoteParameterRule implements Rule +{ + + /** @var Rule|false|null */ + private Rule|false|null $originalRule = null; + + /** + * @param Rule $rule + * @param class-string $nodeType + */ + public function __construct( + private Rule $rule, + private Container $container, + private string $nodeType, + private bool $parameterValue, + private string $parameterName, + ) + { + } + + public function getNodeType(): string + { + return $this->nodeType; + } + + /** + * @return Rule|null + */ + private function getOriginalRule(): ?Rule + { + if ($this->originalRule === false) { + return null; + } + + if ($this->originalRule !== null) { + return $this->originalRule; + } + + $originalRule = null; + try { + /** @var Rule $originalRule */ + $originalRule = $this->container->getByType(get_class($this->rule)); + $taggedRules = $this->container->getServicesByTag(LazyRegistry::RULE_TAG); + $found = false; + foreach ($taggedRules as $rule) { + if ($originalRule !== $rule) { + continue; + } + + $found = true; + break; + } + + if (!$found) { + $originalRule = null; + } + } catch (MissingServiceException) { + // pass + } + + if ($originalRule === null) { + $this->originalRule = false; + return null; + } + + return $this->originalRule = $originalRule; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->parameterValue) { + return []; + } + + if ($this->nodeType !== $this->rule->getNodeType()) { + return []; + } + + $originalRule = $this->getOriginalRule(); + if ($originalRule !== null) { + $originalRuleErrors = $originalRule->processNode($node, $scope); + if (count($originalRuleErrors) > 0) { + 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()); + } + if ($error instanceof FixableNodeRuleError) { + $builder->fixNode($error->getOriginalNode(), $error->getNewNodeCallable()); + } + $errors[] = $builder->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/StaticVarWithoutTypeRule.php b/src/Rules/Playground/StaticVarWithoutTypeRule.php new file mode 100644 index 0000000000..28f5369952 --- /dev/null +++ b/src/Rules/Playground/StaticVarWithoutTypeRule.php @@ -0,0 +1,81 @@ + + */ +final class StaticVarWithoutTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Static_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + $ruleError = RuleErrorBuilder::message('Static variable needs to be typed with PHPDoc @var tag.') + ->identifier('phpstanPlayground.staticWithoutType') + ->build(); + if ($docComment === null) { + return [$ruleError]; + } + $variableNames = []; + foreach ($node->vars as $var) { + if (!is_string($var->var->name)) { + throw new ShouldNotHappenException(); + } + + $variableNames[] = $var->var->name; + } + + $function = $scope->getFunction(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $docComment->getText(), + ); + $varTags = []; + foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { + $varTags[$key] = $varTag; + } + + if (count($varTags) === 0) { + return [$ruleError]; + } + + if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { + return []; + } + + foreach ($variableNames as $variableName) { + if (isset($varTags[$variableName])) { + continue; + } + + return [$ruleError]; + } + + return []; + } + +} diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php index 8accf408b2..44773b9275 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -5,13 +5,16 @@ use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class AccessPrivatePropertyThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class AccessPrivatePropertyThroughStaticRule implements Rule { public function getNodeType(): string @@ -35,17 +38,14 @@ public function processNode(Node $node, Scope $scope): array } $classType = $scope->resolveTypeByName($className); - if (!$classType->hasProperty($propertyName)->yes()) { + if (!$classType->hasStaticProperty($propertyName)->yes()) { return []; } - $property = $classType->getProperty($propertyName, $scope); + $property = $classType->getStaticProperty($propertyName, $scope); if (!$property->isPrivate()) { return []; } - if (!$property->isStatic()) { - return []; - } if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { return []; @@ -55,8 +55,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Unsafe access to private property %s::$%s through static::.', $property->getDeclaringClass()->getDisplayName(), - $propertyName - ))->build(), + $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..82d1753a1d --- /dev/null +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -0,0 +1,311 @@ + + */ + public function check(PropertyFetch $node, Scope $scope, bool $write): array + { + $errors = []; + 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()); + + if (!$write && $this->checkNonStringableDynamicAccess) { + $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(), + ); + $nameType = $nameTypeResult->getType(); + if ( + !$nameType instanceof ErrorType + && ( + $nameType->toString() instanceof ErrorType + || !$nameType->toString()->isString()->yes() + ) + ) { + $originalNameType = $scope->getType($node->name); + $className = $scope->getType($node->var)->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf('Property name for %s must be a string, but %s was given.', $className, $originalNameType->describe(VerbosityLevel::precise()))) + ->identifier('property.nameNotString') + ->build(); + } + } + } + + 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->hasInstanceProperty($name)->yes() || $type->hasStaticProperty($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->hasInstanceProperty($name); + $hasStatic = $type->hasStaticProperty($name); + if ($has->maybe() && !$hasStatic->yes()) { + if ($scope->isUndefinedExpressionAllowed($node)) { + if (!$this->checkDynamicProperties) { + return []; + } + + $maybePropertyReflection = $this->pickProperty($scope, $type, $name); + if ($maybePropertyReflection !== null && $maybePropertyReflection->isDummy()->no()) { + 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->hasInstanceProperty($name)) { + if ($write) { + if ($scope->canWriteProperty($parentClassReflection->getInstanceProperty($name, $scope))) { + return []; + } + } elseif ($scope->canReadProperty($parentClassReflection->getInstanceProperty($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 []; + } + } + + if ($hasStatic->yes()) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-static access to static property %s::$%s.', + $type->getStaticProperty($name, $scope)->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('staticProperty.nonStaticAccess')->build(), + ]; + } + + $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->getInstanceProperty($name, $scope); + if ($write) { + if ($scope->canWriteProperty($propertyReflection)) { + return []; + } + } elseif ($scope->canReadProperty($propertyReflection)) { + return []; + } + + if ( + !$this->phpVersion->supportsAsymmetricVisibility() + || !$write + || (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) + ) { + if ( + $scope->isUndefinedExpressionAllowed($node) + && !$propertyReflection->getDeclaringClass()->isFinal() + ) { + return []; + } + + 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 pickProperty(Scope $scope, Type $type, string $name): ?ExtendedPropertyReflection + { + $types = []; + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + if ($innerType->hasInstanceProperty($name)->no()) { + continue; + } + + $types[] = $innerType; + } + } + + if (count($types) === 0) { + try { + return $type->getInstanceProperty($name, $scope); + } catch (MissingPropertyFromReflectionException) { + return null; + } + } + + if (count($types) === 1) { + try { + return $types[0]->getInstanceProperty($name, $scope); + } catch (MissingPropertyFromReflectionException) { + return null; + } + } + + $unionType = TypeCombinator::union(...$types); + + try { + return $unionType->getInstanceProperty($name, $scope); + } catch (MissingPropertyFromReflectionException) { + return null; + } + } + +} diff --git a/src/Rules/Properties/AccessPropertiesInAssignRule.php b/src/Rules/Properties/AccessPropertiesInAssignRule.php index 2ed10a19fe..e53aff230e 100644 --- a/src/Rules/Properties/AccessPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -4,33 +4,37 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ -class AccessPropertiesInAssignRule implements Rule +#[RegisteredRule(level: 0)] +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 c2d43fab36..27000383b0 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -2,41 +2,21 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; -use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleError; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\VerbosityLevel; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\PropertyFetch> + * @implements Rule */ -class AccessPropertiesRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +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 @@ -44,122 +24,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 Identifier) { - $names = [$node->name->name]; - } else { - $names = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($node->name))); - } - - $errors = []; - foreach ($names as $name) { - $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name)); - } - - return $errors; - } - - /** - * @param Scope $scope - * @param PropertyFetch $node - * @param string $name - * @return RuleError[] - */ - private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array - { - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->var, - sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($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 !== null) { - if ($parentClassReflection->hasProperty($name)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Access to private property $%s of parent class %s.', - $name, - $parentClassReflection->getDisplayName() - ))->build(), - ]; - } - - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } - - 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..71db3bb1e6 100644 --- a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -4,33 +4,37 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ -class AccessStaticPropertiesInAssignRule implements Rule +#[RegisteredRule(level: 0)] +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 7d96f249f3..e85747e84d 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -5,12 +5,17 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantStringType; @@ -21,28 +26,28 @@ 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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -55,9 +60,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { - $names = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); } $errors = []; @@ -69,10 +72,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope - * @param StaticPropertyFetch $node - * @param string $name - * @return RuleError[] + * @return list */ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array { @@ -86,8 +86,8 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name - ))->build(), + $name, + ))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(), ]; } $classType = $scope->resolveTypeByName($node->class); @@ -97,8 +97,8 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name - ))->build(), + $name, + ))->identifier('outOfClass.parent')->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -108,21 +108,11 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $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 []; - } - $classType = $scope->resolveTypeByName($node->class); } else { if (!$this->reflectionProvider->hasClass($class)) { @@ -130,27 +120,42 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Access to static property $%s on an unknown class %s.', + $name, + $class, + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf( - 'Access to static property $%s on an unknown class %s.', - $name, - $class - ))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); } + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($class); + if ($locationClassReflection->hasStaticProperty($name)) { + $locationData['property'] = $locationClassReflection->getStaticProperty($name); + } + + $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, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), - static function (Type $type) use ($name): bool { - return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); - } + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($name)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -158,7 +163,7 @@ static function (Type $type) use ($name): bool { } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return []; } @@ -172,56 +177,84 @@ static function (Type $type) use ($name): bool { 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->hasStaticProperty($name); + if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) { + return []; + } + + if (!$has->yes()) { + if ($scope->hasExpressionType($node)->yes()) { return $messages; } - return array_merge($messages, [ - RuleErrorBuilder::message(sprintf( - 'Access to an undefined static property %s::$%s.', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $name - ))->build(), - ]); - } + $classNames = $classType->getObjectClassNames(); + if (count($classNames) === 1) { + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); + $parentClassReflection = $propertyClassReflection->getParentClass(); - $property = $classType->getProperty($name, $scope); - if (!$property->isStatic()) { - $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); - foreach ($hasPropertyTypes as $hasPropertyType) { - if ($hasPropertyType->getPropertyName() === $name) { - return []; + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasStaticProperty($name)) { + if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name))) { + 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(); } } + if ($classType->hasInstanceProperty($name)->yes()) { + $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); + foreach ($hasPropertyTypes as $hasPropertyType) { + if ($hasPropertyType->getPropertyName() === $name) { + return []; + } + } + + return array_merge($messages, [ + RuleErrorBuilder::message(sprintf( + 'Static access to instance property %s::$%s.', + $classType->getInstanceProperty($name, $scope)->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('property.staticAccess')->build(), + ]); + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( - 'Static access to instance property %s::$%s.', - $property->getDeclaringClass()->getDisplayName(), - $name - ))->build(), + 'Access to an undefined static property %s::$%s.', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier('staticProperty.notFound')->build(), ]); } - if (!$scope->canAccessProperty($property)) { + $property = $classType->getStaticProperty($name, $scope); + 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 24d929355e..ed947b1a67 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -4,23 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -class DefaultValueTypesAssignedToPropertiesRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 3)] +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 @@ -30,25 +30,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); $default = $node->getDefault(); if ($default === null) { return []; } + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getWritableType(); - if ($propertyReflection->getNativeType() instanceof MixedType) { - if ($default instanceof Node\Expr\ConstFetch && (string) $default->name === 'null') { + if (!$propertyReflection->hasNativeType()) { + if ($default instanceof Node\Expr\ConstFetch && $default->name->toLowerString() === 'null') { return []; } } $defaultValueType = $scope->getType($default); - if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true)) { + $accepts = $this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true); + if ($accepts->result) { return []; } @@ -61,8 +59,11 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $propertyType->describe($verbosityLevel), - $defaultValueType->describe($verbosityLevel) - ))->build(), + $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 6a4c1f1cf1..ae9a7af28b 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -4,37 +4,41 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -class ExistingClassesInPropertiesRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter] + private bool $checkThisOnly, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string @@ -44,17 +48,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); + $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(), ); } @@ -66,27 +66,48 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has invalid type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $referencedClass - ))->build(); + $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->getName(), - $referencedClass - ))->discoveringSymbolsTip()->build(); + $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..96f326e17a --- /dev/null +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -0,0 +1,97 @@ + + */ +#[RegisteredRule(level: 0)] +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, + ), + // Should be impossible, property hooks do not support return types + sprintf( + 'Impossible condition: Attribute NoDiscard cannot be used on void %s hook for property %s::$%s.', + ucfirst($hookName), + $className, + $propertyName, + ), + ); + } + +} diff --git a/src/Rules/Properties/FoundPropertyReflection.php b/src/Rules/Properties/FoundPropertyReflection.php index 97efdcd412..2ac80dd736 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -4,38 +4,24 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class FoundPropertyReflection implements PropertyReflection +final class FoundPropertyReflection implements ExtendedPropertyReflection { - private PropertyReflection $originalPropertyReflection; - - private Scope $scope; - - private string $propertyName; - - private Type $readableType; - - private Type $writableType; - public function __construct( - PropertyReflection $originalPropertyReflection, - Scope $scope, - string $propertyName, - Type $readableType, - Type $writableType + private ExtendedPropertyReflection $originalPropertyReflection, + private Scope $scope, + private string $propertyName, + private Type $readableType, + private Type $writableType, ) { - $this->originalPropertyReflection = $originalPropertyReflection; - $this->scope = $scope; - $this->propertyName = $propertyName; - $this->readableType = $readableType; - $this->writableType = $writableType; } public function getScope(): Scope @@ -73,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; @@ -115,15 +121,10 @@ public function isInternal(): TrinaryLogic public function isNative(): bool { - $reflection = $this->originalPropertyReflection; - while ($reflection instanceof WrapperPropertyReflection) { - $reflection = $reflection->getOriginalReflection(); - } - - return $reflection instanceof PhpPropertyReflection; + return $this->getNativeReflection() !== null; } - public function getNativeType(): ?Type + public function getNativeReflection(): ?PhpPropertyReflection { $reflection = $this->originalPropertyReflection; while ($reflection instanceof WrapperPropertyReflection) { @@ -134,7 +135,57 @@ public function getNativeType(): ?Type return null; } - return $reflection->getNativeType(); + return $reflection; + } + + public function isAbstract(): TrinaryLogic + { + return $this->originalPropertyReflection->isAbstract(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->originalPropertyReflection->isFinalByKeyword(); + } + + 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(); + } + + public function isDummy(): TrinaryLogic + { + return $this->originalPropertyReflection->isDummy(); } } diff --git a/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php new file mode 100644 index 0000000000..188f97c552 --- /dev/null +++ b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php @@ -0,0 +1,134 @@ + + */ +#[RegisteredRule(level: 3)] +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..e0fe89a5b3 --- /dev/null +++ b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php @@ -0,0 +1,67 @@ + + */ +#[RegisteredRule(level: 0)] +final class InvalidCallablePropertyTypeRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if (!$propertyReflection->hasNativeType()) { + return []; + } + + $nativeType = $propertyReflection->getNativeType(); + $callableTypes = []; + + TypeTraverser::map($nativeType, static function (Type $type, callable $traverse) use (&$callableTypes): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof CallableType) { + $callableTypes[] = $type; + } + + return $type; + }); + + if ($callableTypes === []) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Property %s::$%s cannot have callable in its type declaration.', + $classReflection->getDisplayName(), + $node->getName(), + ))->identifier('property.callableType')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php index eed89ae937..603c685105 100644 --- a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php @@ -2,28 +2,23 @@ namespace PHPStan\Rules\Properties; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider +#[AutowiredService] +final class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { - private Container $container; - /** @var ReadWritePropertiesExtension[]|null */ private ?array $extensions = null; - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getExtensions(): array { - if ($this->extensions === null) { - $this->extensions = $this->container->getServicesByTag(ReadWritePropertiesExtensionProvider::EXTENSION_TAG); - } - - return $this->extensions; + return $this->extensions ??= $this->container->getServicesByTag(ReadWritePropertiesExtensionProvider::EXTENSION_TAG); } } diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index 04a2d25fed..450b6af0ea 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -4,23 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -final class MissingPropertyTypehintRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 6)] +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 @@ -30,19 +31,21 @@ public function getNodeType(): string 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->getName()); $propertyType = $propertyReflection->getReadableType(); + if ($propertyType instanceof MixedType && !$propertyType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( 'Property %s::$%s has no type specified.', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->build(), + $node->getName(), + ))->identifier('missingType.property')->build(), ]; } @@ -53,8 +56,11 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s type has no value type specified in iterable type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $iterableTypeDescription - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($propertyType) as [$name, $genericTypeNames]) { @@ -63,8 +69,10 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames) - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($propertyType) as $callableType) { @@ -72,8 +80,8 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s type has no signature specified for %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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..35bedec527 --- /dev/null +++ b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php @@ -0,0 +1,81 @@ + + */ +#[RegisteredRule(level: 0)] +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..1f2c1ff721 --- /dev/null +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -0,0 +1,84 @@ + + */ +#[RegisteredRule(level: 0)] +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 index 803eabf0f2..5348cb351d 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -4,17 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule */ -class NullsafePropertyFetchRule implements Rule +#[RegisteredRule(level: 4)] +final class NullsafePropertyFetchRule implements Rule { + public function __construct() + { + } + public function getNodeType(): string { return Node\Expr\NullsafePropertyFetch::class; @@ -22,18 +28,19 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { + if (!$calledOnType->isNull()->no()) { return []; } - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if ($scope->isUndefinedExpressionAllowed($node)) { return []; } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index ad45a22d52..0def12649a 100644 --- a/src/Rules/Properties/OverridingPropertyRule.php +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -4,31 +4,34 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function count; +use function sprintf; /** * @implements Rule */ -class OverridingPropertyRule implements Rule +#[RegisteredRule(level: 0)] +final class OverridingPropertyRule implements Rule { - private bool $checkPhpDocMethodSignatures; - - private bool $reportMaybes; - public function __construct( - bool $checkPhpDocMethodSignatures, - bool $reportMaybes + private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $checkPhpDocMethodSignatures, + #[AutowiredParameter(ref: '%reportMaybesInPropertyPhpDocTypes%')] + private bool $reportMaybes, ) { - $this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -38,11 +41,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $prototype = $this->findPrototype($classReflection, $node->getName()); if ($prototype === null) { return []; @@ -56,8 +55,8 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.nonStatic')->nonIgnorable()->build(); } } elseif ($node->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -65,8 +64,8 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.static')->nonIgnorable()->build(); } if ($prototype->isReadOnly()) { @@ -76,17 +75,48 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.readWrite')->nonIgnorable()->build(); } } elseif ($node->isReadOnly()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Readonly property %s::$%s overrides readwrite property %s::$%s.', - $classReflection->getDisplayName(), - $node->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + if ( + !$this->phpVersion->supportsPropertyHooks() + || $prototype->isWritable() + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readonly property %s::$%s overrides readwrite property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.readOnly')->nonIgnorable()->build(); + } + } + + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + if ($this->phpVersion->supportsPropertyHooks()) { + if ($prototype->isReadable()) { + if (!$propertyReflection->isReadable()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding readable property %s::$%s also has to be readable.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.notReadable')->nonIgnorable()->build(); + } + } + if ($prototype->isWritable()) { + if (!$propertyReflection->isWritable()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding writable property %s::$%s also has to be writable.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.notWritable')->nonIgnorable()->build(); + } + } } if ($prototype->isPublic()) { @@ -97,8 +127,8 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.visibility')->nonIgnorable()->build(); } } elseif ($node->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -106,13 +136,35 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.visibility')->nonIgnorable()->build(); + } + + if ($prototype->isFinalByKeyword()->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(); + } elseif ($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.parentPropertyFinalByPhpDoc') + ->build(); } $typeErrors = []; + $nativeType = $node->getNativeType(); if ($prototype->hasNativeType()) { - if ($node->getNativeType() === null) { + if ($nativeType === null) { $typeErrors[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s overriding property %s::$%s (%s) should also have native type %s.', $classReflection->getDisplayName(), @@ -120,31 +172,60 @@ public function processNode(Node $node, Scope $scope): array $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()) - ))->nonIgnorable()->build(); + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.missingNativeType')->nonIgnorable()->build(); } else { - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()); if (!$prototype->getNativeType()->equals($nativeType)) { - $typeErrors[] = RuleErrorBuilder::message(sprintf( - 'Type %s of property %s::$%s is not the same as type %s of overridden property %s::$%s.', - $nativeType->describe(VerbosityLevel::typeOnly()), - $classReflection->getDisplayName(), - $node->getName(), - $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + if ( + $this->phpVersion->supportsPropertyHooks() + && ($prototype->isVirtual()->yes() || $prototype->isAbstract()->yes()) + && (!$prototype->isReadable() || !$prototype->isWritable()) + ) { + if (!$prototype->isReadable()) { + if (!$nativeType->isSuperTypeOf($prototype->getNativeType())->yes()) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of property %s::$%s is not contravariant with type %s of overridden property %s::$%s.', + $nativeType->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } + } elseif (!$prototype->getNativeType()->isSuperTypeOf($nativeType)->yes()) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of property %s::$%s is not covariant with type %s of overridden property %s::$%s.', + $nativeType->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } + } else { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of property %s::$%s is not the same as type %s of overridden property %s::$%s.', + $nativeType->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } } } - } elseif ($node->getNativeType() !== null) { + } elseif ($nativeType !== null) { $typeErrors[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s (%s) overriding property %s::$%s should not have a native type.', $classReflection->getDisplayName(), $node->getName(), - ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection())->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->nonIgnorable()->build(); + $node->getName(), + ))->identifier('property.extraNativeType')->nonIgnorable()->build(); } $errors = array_merge($errors, $typeErrors); @@ -157,12 +238,50 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - $propertyReflection = $classReflection->getNativeProperty($node->getName()); if ($prototype->getReadableType()->equals($propertyReflection->getReadableType())) { return $errors; } $verbosity = VerbosityLevel::getRecommendedLevelByType($prototype->getReadableType(), $propertyReflection->getReadableType()); + + if ( + $this->phpVersion->supportsPropertyHooks() + && ($prototype->isVirtual()->yes() || $prototype->isAbstract()->yes()) + && (!$prototype->isReadable() || !$prototype->isWritable()) + ) { + if (!$prototype->isReadable()) { + if (!$propertyReflection->getReadableType()->isSuperTypeOf($prototype->getReadableType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is not contravariant with PHPDoc type %s of overridden property %s::$%s.', + $propertyReflection->getReadableType()->describe($verbosity), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getReadableType()->describe($verbosity), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.phpDocType')->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ))->build(); + } + } elseif (!$prototype->getReadableType()->isSuperTypeOf($propertyReflection->getReadableType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is not covariant with PHPDoc type %s of overridden property %s::$%s.', + $propertyReflection->getReadableType()->describe($verbosity), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getReadableType()->describe($verbosity), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.phpDocType')->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ))->build(); + } + + return $errors; + } + $isSuperType = $prototype->getReadableType()->isSuperTypeOf($propertyReflection->getReadableType()); $canBeTurnedOffError = RuleErrorBuilder::message(sprintf( 'PHPDoc type %s of property %s::$%s is not the same as PHPDoc type %s of overridden property %s::$%s.', @@ -171,11 +290,11 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->tip(sprintf( + $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%.' + '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.', @@ -185,10 +304,10 @@ public function processNode(Node $node, Scope $scope): array $this->reportMaybes ? 'not the same as' : 'not covariant with', $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), - $node->getName() - ))->tip(sprintf( + $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' + 'https://phpstan.org/user-guide/stub-files', ))->build(); if ($this->reportMaybes) { if (!$isSuperType->yes()) { @@ -209,19 +328,32 @@ private function findPrototype(ClassReflection $classReflection, string $propert { $parentClass = $classReflection->getParentClass(); if ($parentClass === null) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } if (!$parentClass->hasNativeProperty($propertyName)) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } $property = $parentClass->getNativeProperty($propertyName); if ($property->isPrivate()) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } return $property; } + private function findPrototypeInInterfaces(ClassReflection $classReflection, string $propertyName): ?PhpPropertyReflection + { + foreach ($classReflection->getInterfaces() as $interface) { + if (!$interface->hasNativeProperty($propertyName)) { + continue; + } + + return $interface->getNativeProperty($propertyName); + } + + return null; + } + } diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php new file mode 100644 index 0000000000..223623f069 --- /dev/null +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -0,0 +1,134 @@ + + */ +#[RegisteredRule(level: 0)] +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..c61bceff27 --- /dev/null +++ b/src/Rules/Properties/PropertyAssignRefRule.php @@ -0,0 +1,73 @@ + + */ +#[RegisteredRule(level: 0)] +final class PropertyAssignRefRule implements Rule +{ + + public function __construct( + private PhpVersion $phpVersion, + private PropertyReflectionFinder $propertyReflectionFinder, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\AssignRef::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsAsymmetricVisibility()) { + return []; + } + + if (!$node->expr instanceof Node\Expr\PropertyFetch) { + return []; + } + + $propertyFetch = $node->expr; + + $errors = []; + $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + foreach ($reflections as $propertyReflection) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null) { + continue; + } + if ($scope->canWriteProperty($propertyReflection)) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s with %s visibility is assigned by reference.', + $declaringClass->getDisplayName(), + $propertyReflection->getName(), + $propertyReflection->isPrivateSet() ? 'private(set)' : ( + $propertyReflection->isProtectedSet() ? 'protected(set)' : ( + $propertyReflection->isPrivate() ? 'private' : 'protected' + ) + ), + )) + ->identifier('property.assignByRef') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index d00a080e8f..2a77c1e01a 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -2,22 +2,22 @@ namespace PHPStan\Rules\Properties; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** * @implements Rule */ -class PropertyAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class PropertyAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +30,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_PROPERTY, - 'property' + Attribute::TARGET_PROPERTY, + 'property', ); } diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index e8e1a74a24..d9d6e10c99 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -2,34 +2,42 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\ObjectType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; -class PropertyDescriptor +#[AutowiredService] +final class PropertyDescriptor { - public function describePropertyByName(PropertyReflection $property, string $propertyName): string - { - if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - /** - * @param \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 (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Property %s::$%s', $classDescription, $name->name); } - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Static property %s::$%s', $classDescription, $name->name); } } diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php new file mode 100644 index 0000000000..2eb1c11f60 --- /dev/null +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -0,0 +1,59 @@ + + */ +#[RegisteredRule(level: 0)] +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 + { + $attrGroups = $node->getOriginalNode()->attrGroups; + $errors = $this->attributesCheck->check( + $scope, + $attrGroups, + Attribute::TARGET_METHOD, + 'method', + ); + + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + $name = $attribute->name->toString(); + if (strtolower($name) === 'nodiscard') { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s cannot be used on property hooks.', $name)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->nonIgnorable() + ->build(); + break; + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php new file mode 100644 index 0000000000..63dc0da358 --- /dev/null +++ b/src/Rules/Properties/PropertyInClassRule.php @@ -0,0 +1,217 @@ + + */ +#[RegisteredRule(level: 0)] +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 3e07277ada..b25682687b 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -2,43 +2,44 @@ 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\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use function array_map; +use function count; -class PropertyReflectionFinder +#[AutowiredService] +final class PropertyReflectionFinder { /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @param \PHPStan\Analyser\Scope $scope + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch * @return FoundPropertyReflection[] */ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): array { - if ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - if ($propertyFetch->name instanceof \PhpParser\Node\Identifier) { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + if ($propertyFetch->name instanceof Node\Identifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static function (ConstantStringType $name): string { - return $name->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; $propertyHolderType = $scope->getType($propertyFetch->var); foreach ($names as $name) { - $reflection = $this->findPropertyReflection( + $reflection = $this->findInstancePropertyReflection( $propertyHolderType, $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( $propertyFetch->name, - new String_($name) - )) : $scope + new String_($name), + )) : $scope, ); if ($reflection === null) { continue; @@ -50,7 +51,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a return $reflections; } - if ($propertyFetch->class instanceof \PhpParser\Node\Name) { + if ($propertyFetch->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); @@ -59,20 +60,18 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof VarLikeIdentifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static function (ConstantStringType $name): string { - return $name->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; foreach ($names as $name) { - $reflection = $this->findPropertyReflection( + $reflection = $this->findStaticPropertyReflection( $propertyHolderType, $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( $propertyFetch->name, - new String_($name) - )) : $scope + new String_($name), + )) : $scope, ); if ($reflection === null) { continue; @@ -85,47 +84,69 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a } /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @param \PHPStan\Analyser\Scope $scope - * @return FoundPropertyReflection|null + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection { - if ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { - return null; - } + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { $propertyHolderType = $scope->getType($propertyFetch->var); - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + if ($propertyFetch->name instanceof Node\Identifier) { + return $this->findInstancePropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + } + + $nameType = $scope->getType($propertyFetch->name); + $nameTypeConstantStrings = $nameType->getConstantStrings(); + if (count($nameTypeConstantStrings) === 1) { + return $this->findInstancePropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + } + + return null; } - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { + if (!$propertyFetch->name instanceof Node\Identifier) { return null; } - if ($propertyFetch->class instanceof \PhpParser\Node\Name) { + if ($propertyFetch->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); } - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + return $this->findStaticPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + } + + private function findInstancePropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection + { + if (!$propertyHolderType->hasInstanceProperty($propertyName)->yes()) { + return null; + } + + $originalProperty = $propertyHolderType->getInstanceProperty($propertyName, $scope); + + return new FoundPropertyReflection( + $originalProperty, + $scope, + $propertyName, + $originalProperty->getReadableType(), + $originalProperty->getWritableType(), + ); } - private function findPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection + private function findStaticPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection { - if (!$propertyHolderType->hasProperty($propertyName)->yes()) { + if (!$propertyHolderType->hasStaticProperty($propertyName)->yes()) { return null; } - $originalProperty = $propertyHolderType->getProperty($propertyName, $scope); + $originalProperty = $propertyHolderType->getStaticProperty($propertyName, $scope); return new FoundPropertyReflection( $originalProperty, $scope, $propertyName, $originalProperty->getReadableType(), - $originalProperty->getWritableType() + $originalProperty->getWritableType(), ); } diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php new file mode 100644 index 0000000000..3f0911fe37 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php @@ -0,0 +1,59 @@ + + */ +#[RegisteredRule(level: 3)] +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..4f2eb63a76 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php @@ -0,0 +1,131 @@ + + */ +#[RegisteredRule(level: 3)] +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..f45c6a701e --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -0,0 +1,40 @@ + + */ +#[RegisteredRule(level: 0)] +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..08a5db774b --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -0,0 +1,59 @@ + + */ +#[RegisteredRule(level: 3)] +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..75e52f49d8 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -0,0 +1,114 @@ + + */ +#[RegisteredRule(level: 3)] +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 index 16162f53bb..23f958bf74 100644 --- a/src/Rules/Properties/ReadOnlyPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; @@ -12,14 +13,12 @@ /** * @implements Rule */ -class ReadOnlyPropertyRule implements Rule +#[RegisteredRule(level: 0)] +final class ReadOnlyPropertyRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -35,15 +34,28 @@ public function processNode(Node $node, Scope $scope): array $errors = []; if (!$this->phpVersion->supportsReadOnlyProperties()) { - $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable() + ->identifier('property.readOnlyNotSupported') + ->build(); } if ($node->getNativeType() === null) { - $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.') + ->identifier('property.readOnlyNoNativeType') + ->nonIgnorable() + ->build(); } if ($node->getDefault() !== null) { - $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable() + ->identifier('property.readOnlyDefaultValue') + ->build(); + } + + if ($node->isStatic()) { + $errors[] = RuleErrorBuilder::message('Readonly property cannot be static.')->nonIgnorable() + ->identifier('property.readOnlyStatic') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadWritePropertiesExtension.php b/src/Rules/Properties/ReadWritePropertiesExtension.php index b8f26e60c7..804619781c 100644 --- a/src/Rules/Properties/ReadWritePropertiesExtension.php +++ b/src/Rules/Properties/ReadWritePropertiesExtension.php @@ -2,16 +2,33 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-read or always-written properties. + * + * To register it in the configuration file use the `phpstan.properties.readWriteExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.properties.readWriteExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-read-written-properties + * + * @api + */ interface ReadWritePropertiesExtension { - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool; + public function isAlwaysRead(ExtendedPropertyReflection $property, string $propertyName): bool; - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool; + public function isAlwaysWritten(ExtendedPropertyReflection $property, string $propertyName): bool; - public function isInitialized(PropertyReflection $property, string $propertyName): bool; + public function isInitialized(ExtendedPropertyReflection $property, string $propertyName): bool; } diff --git a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php index 2b47df73bd..daecd99b68 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -4,39 +4,33 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class ReadingWriteOnlyPropertiesRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + 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 +58,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..e7a4ac5d88 --- /dev/null +++ b/src/Rules/Properties/SetNonVirtualPropertyHookAssignRule.php @@ -0,0 +1,95 @@ + + */ +#[RegisteredRule(level: 3)] +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..82f89362b8 --- /dev/null +++ b/src/Rules/Properties/SetPropertyHookParameterRule.php @@ -0,0 +1,162 @@ + + */ +#[RegisteredRule(level: 0)] +final class SetPropertyHookParameterRule implements Rule +{ + + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + #[AutowiredParameter] + private bool $checkPhpDocMethodSignatures, + #[AutowiredParameter] + 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 1a123ffdea..472ce3568e 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -3,66 +3,52 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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 - && !$node instanceof Node\Expr\AssignRef - ) { - return []; - } - - if ( - !($node->var instanceof Node\Expr\PropertyFetch) - && !($node->var instanceof Node\Expr\StaticPropertyFetch) - ) { - return []; - } - - /** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ - $propertyFetch = $node->var; + $propertyFetch = $node->getPropertyFetch(); $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); $errors = []; foreach ($propertyReflections as $propertyReflection) { $errors = array_merge($errors, $this->processSingleProperty( $propertyReflection, - $node + $propertyFetch, + $node->getAssignedExpr(), )); } @@ -70,25 +56,40 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param FoundPropertyReflection $propertyReflection - * @param Node\Expr $node - * @return RuleError[] + * @return list */ private function processSingleProperty( FoundPropertyReflection $propertyReflection, - Node\Expr $node + PropertyFetch|StaticPropertyFetch $fetch, + Node\Expr $assignedExpr, ): array { - $propertyType = $propertyReflection->getWritableType(); - $scope = $propertyReflection->getScope(); + if (!$propertyReflection->isWritable()) { + return []; + } - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) { - $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->describePropertyByName($propertyReflection, $propertyReflection->getName()); + + $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 [ @@ -96,12 +97,24 @@ private function processSingleProperty( '%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 index ca4d9a8118..525a9ecbb6 100644 --- a/src/Rules/Properties/UninitializedPropertyRule.php +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -5,34 +5,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertiesNode; -use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule */ -class UninitializedPropertyRule implements Rule +final class UninitializedPropertyRule implements Rule { - private ReadWritePropertiesExtensionProvider $extensionProvider; - - /** @var string[] */ - private array $additionalConstructors; - - /** @var array */ - private array $additionalConstructorsCache = []; - - /** - * @param string[] $additionalConstructors - */ public function __construct( - ReadWritePropertiesExtensionProvider $extensionProvider, - array $additionalConstructors + private ConstructorsHelper $constructorsHelper, ) { - $this->extensionProvider = $extensionProvider; - $this->additionalConstructors = $additionalConstructors; } public function getNodeType(): string @@ -42,73 +29,40 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->getConstructors($classReflection), $this->extensionProvider->getExtensions()); + $classReflection = $node->getClassReflection(); + [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; foreach ($properties as $propertyName => $propertyNode) { + if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { + continue; + } $errors[] = RuleErrorBuilder::message(sprintf( 'Class %s has an uninitialized property $%s. Give it default value or assign it in the constructor.', $classReflection->getDisplayName(), - $propertyName - ))->line($propertyNode->getLine())->build(); + $propertyName, + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitialized') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { + if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { + continue; + } $errors[] = RuleErrorBuilder::message(sprintf( 'Access to an uninitialized property %s::$%s.', $classReflection->getDisplayName(), - $propertyName - ))->line($line)->build(); + $propertyName, + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitialized') + ->build(); } return $errors; } - /** - * @param ClassReflection $classReflection - * @return string[] - */ - private function getConstructors(ClassReflection $classReflection): array - { - if (array_key_exists($classReflection->getName(), $this->additionalConstructorsCache)) { - return $this->additionalConstructorsCache[$classReflection->getName()]; - } - $constructors = []; - if ($classReflection->hasConstructor()) { - $constructors[] = $classReflection->getConstructor()->getName(); - } - - $nativeReflection = $classReflection->getNativeReflection(); - foreach ($this->additionalConstructors as $additionalConstructor) { - [$className, $methodName] = explode('::', $additionalConstructor); - if (!$nativeReflection->hasMethod($methodName)) { - continue; - } - $nativeMethod = $nativeReflection->getMethod($methodName); - if ($nativeMethod->getDeclaringClass()->getName() !== $nativeReflection->getName()) { - continue; - } - - try { - $prototype = $nativeMethod->getPrototype(); - } catch (\ReflectionException $e) { - $prototype = $nativeMethod; - } - - if ($prototype->getDeclaringClass()->getName() !== $className) { - continue; - } - - $constructors[] = $methodName; - } - - $this->additionalConstructorsCache[$classReflection->getName()] = $constructors; - - return $constructors; - } - } diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index ef79346ab0..8ac259be31 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,85 +4,64 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +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, + #[AutowiredParameter] + 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 - && !$node instanceof Node\Expr\AssignRef - ) { - return []; - } - - if ( - !($node->var instanceof Node\Expr\PropertyFetch) - && !($node->var instanceof Node\Expr\StaticPropertyFetch) - ) { - return []; - } - - if ( - $node->var instanceof Node\Expr\PropertyFetch + $propertyFetch instanceof Node\Expr\PropertyFetch && $this->checkThisOnly - && !$this->ruleLevelHelper->isThis($node->var->var) + && !$this->ruleLevelHelper->isThis($propertyFetch->var) ) { return []; } - /** @var \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..1e5f7a32d6 --- /dev/null +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -0,0 +1,156 @@ + + */ + 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 + && ( + !$functionReflection instanceof ExtendedMethodReflection + || $functionReflection->isFinal()->yes() + || $functionReflection->getDeclaringClass()->isFinal() + ) + ) { + $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..56ec8aa269 --- /dev/null +++ b/src/Rules/Pure/PureFunctionRule.php @@ -0,0 +1,45 @@ + + */ +#[RegisteredRule(level: 2)] +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..9bb11a7e6f --- /dev/null +++ b/src/Rules/Pure/PureMethodRule.php @@ -0,0 +1,45 @@ + + */ +#[RegisteredRule(level: 2)] +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 a797073786..b3a1f98d5d 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -2,19 +2,33 @@ 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\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +final class RegularExpressionPatternRule implements Rule { + public function __construct( + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + public function getNodeType(): string { return FuncCall::class; @@ -31,15 +45,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,7 +60,7 @@ 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 []; } @@ -60,51 +72,48 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array $patternStrings = []; - foreach (TypeUtils::getConstantStrings($patternType) as $constantStringType) { - if ( - !in_array($functionName, [ - 'preg_match', - 'preg_match_all', - 'preg_split', - 'preg_grep', - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { - continue; + if ( + in_array($functionName, [ + 'preg_match', + 'preg_match_all', + 'preg_split', + 'preg_grep', + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + if ($patternNode instanceof Node\Expr\BinaryOp\Concat) { + $patternType = $this->regexExpressionHelper->resolvePatternConcat($patternNode, $scope); + } + foreach ($patternType->getConstantStrings() as $constantStringType) { + $patternStrings[] = $constantStringType->getValue(); } - - $patternStrings[] = $constantStringType->getValue(); } - foreach (TypeUtils::getConstantArrays($patternType) as $constantArrayType) { - if ( - in_array($functionName, [ - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { + if ( + in_array($functionName, [ + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + foreach ($patternType->getConstantArrays() as $constantArrayType) { foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); } - - $patternStrings[] = $arrayKeyType->getValue(); } } + } - if ($functionName !== 'preg_replace_callback_array') { - continue; - } - - foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + if ($functionName === 'preg_replace_callback_array') { + foreach ($patternType->getConstantArrays() as $constantArrayType) { + foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); + } } - - $patternStrings[] = $arrayKeyType->getValue(); } } @@ -114,8 +123,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..210b8d7518 --- /dev/null +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -0,0 +1,244 @@ + + */ +#[RegisteredRule(level: 5)] +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 @@ + + */ +#[AutowiredService] +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; + } + + if ($classReflection->getName() !== $constantReflection->getDeclaringClass()->getName()) { + $rewrittenConstantReflection = new RewrittenDeclaringClassClassConstantReflection($classReflection, $constantReflection); + $rewrittenRestrictedUsage = $extension->isRestrictedClassConstantUsage($rewrittenConstantReflection, $scope); + if ($rewrittenRestrictedUsage === 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 @@ + + */ +#[AutowiredService] +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 @@ + + */ +#[AutowiredService] +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..c9f1861b7c --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -0,0 +1,81 @@ + + */ +#[AutowiredService] +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 @@ + + */ +#[AutowiredService] +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 @@ + + */ +#[AutowiredService] +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->hasInstanceProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getInstanceProperty($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..22230ca299 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -0,0 +1,109 @@ + + */ +#[AutowiredService] +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; + } + + if ($classReflection->getName() !== $methodReflection->getDeclaringClass()->getName()) { + $rewrittenMethodReflection = new RewrittenDeclaringClassMethodReflection($classReflection, $methodReflection); + $rewrittenRestrictedUsage = $extension->isRestrictedMethodUsage($rewrittenMethodReflection, $scope); + if ($rewrittenRestrictedUsage === 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..898075b514 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -0,0 +1,108 @@ + + */ +#[AutowiredService] +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; + } + + if ($classReflection->getName() !== $methodReflection->getDeclaringClass()->getName()) { + $rewrittenMethodReflection = new RewrittenDeclaringClassMethodReflection($classReflection, $methodReflection); + $rewrittenRestrictedUsage = $extension->isRestrictedMethodUsage($rewrittenMethodReflection, $scope); + if ($rewrittenRestrictedUsage === 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..6361276a20 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -0,0 +1,108 @@ + + */ +#[AutowiredService] +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->hasInstanceProperty($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->hasStaticProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getStaticProperty($propertyName); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + if ($classReflection->getName() !== $propertyReflection->getDeclaringClass()->getName()) { + $rewrittenPropertyReflection = new RewrittenDeclaringClassPropertyReflection($classReflection, $propertyReflection); + $rewrittenRestrictedUsage = $extension->isRestrictedPropertyUsage($rewrittenPropertyReflection, $scope); + if ($rewrittenRestrictedUsage === 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 @@ + + */ +#[AutowiredService] +final class RestrictedUsageOfDeprecatedStringCastRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Cast\String_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + /** @var RestrictedMethodUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $exprType = $scope->getType($node->expr); + $referencedClasses = $exprType->getObjectClassNames(); + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasNativeMethod('__toString')) { + continue; + } + + $methodReflection = $classReflection->getNativeMethod('__toString'); + 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/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php new file mode 100644 index 0000000000..b7a102db9f --- /dev/null +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -0,0 +1,111 @@ +constantReflection->getValueExpr(); + } + + public function isFinal(): bool + { + return $this->constantReflection->isFinal(); + } + + public function hasPhpDocType(): bool + { + return $this->constantReflection->hasPhpDocType(); + } + + public function getPhpDocType(): ?Type + { + return $this->constantReflection->getPhpDocType(); + } + + public function hasNativeType(): bool + { + return $this->constantReflection->hasNativeType(); + } + + public function getNativeType(): ?Type + { + return $this->constantReflection->getNativeType(); + } + + public function getAttributes(): array + { + return $this->constantReflection->getAttributes(); + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return $this->constantReflection->isStatic(); + } + + public function isPrivate(): bool + { + return $this->constantReflection->isPrivate(); + } + + public function isPublic(): bool + { + return $this->constantReflection->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->constantReflection->getDocComment(); + } + + public function getName(): string + { + return $this->constantReflection->getName(); + } + + public function getValueType(): Type + { + return $this->constantReflection->getValueType(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->constantReflection->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->constantReflection->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->constantReflection->isInternal(); + } + + public function getFileName(): ?string + { + return $this->constantReflection->getFileName(); + } + +} diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php new file mode 100644 index 0000000000..c834dfd5b1 --- /dev/null +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -0,0 +1,153 @@ +declaringClass; + } + + public function isStatic(): bool + { + return $this->methodReflection->isStatic(); + } + + public function isPrivate(): bool + { + return $this->methodReflection->isPrivate(); + } + + public function isPublic(): bool + { + return $this->methodReflection->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->methodReflection->getDocComment(); + } + + public function getVariants(): array + { + return $this->methodReflection->getVariants(); + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->methodReflection->getOnlyVariant(); + } + + public function getNamedArgumentsVariants(): ?array + { + return $this->methodReflection->getNamedArgumentsVariants(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->methodReflection->acceptsNamedArguments(); + } + + public function getAsserts(): Assertions + { + return $this->methodReflection->getAsserts(); + } + + public function getSelfOutType(): ?Type + { + return $this->methodReflection->getSelfOutType(); + } + + public function returnsByReference(): TrinaryLogic + { + return $this->methodReflection->returnsByReference(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->methodReflection->isFinalByKeyword(); + } + + public function isAbstract(): TrinaryLogic|bool + { + return $this->methodReflection->isAbstract(); + } + + public function isBuiltin(): TrinaryLogic|bool + { + return $this->methodReflection->isBuiltin(); + } + + public function isPure(): TrinaryLogic + { + return $this->methodReflection->isPure(); + } + + public function getAttributes(): array + { + return $this->methodReflection->getAttributes(); + } + + public function getName(): string + { + return $this->methodReflection->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->methodReflection->getPrototype(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->methodReflection->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->methodReflection->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->methodReflection->isFinal(); + } + + public function isInternal(): TrinaryLogic + { + return $this->methodReflection->isInternal(); + } + + public function getThrowType(): ?Type + { + return $this->methodReflection->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->methodReflection->hasSideEffects(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->methodReflection->mustUseReturnValue(); + } + +} diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php new file mode 100644 index 0000000000..df84f51249 --- /dev/null +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php @@ -0,0 +1,161 @@ +declaringClass; + } + + public function isStatic(): bool + { + return $this->propertyReflection->isStatic(); + } + + public function isPrivate(): bool + { + return $this->propertyReflection->isPrivate(); + } + + public function isPublic(): bool + { + return $this->propertyReflection->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->propertyReflection->getDocComment(); + } + + public function getName(): string + { + return $this->propertyReflection->getName(); + } + + public function hasPhpDocType(): bool + { + return $this->propertyReflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->propertyReflection->getPhpDocType(); + } + + public function hasNativeType(): bool + { + return $this->propertyReflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->propertyReflection->getNativeType(); + } + + public function isAbstract(): TrinaryLogic + { + return $this->propertyReflection->isAbstract(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->propertyReflection->isFinalByKeyword(); + } + + public function isFinal(): TrinaryLogic + { + return $this->propertyReflection->isFinal(); + } + + public function isVirtual(): TrinaryLogic + { + return $this->propertyReflection->isVirtual(); + } + + public function hasHook(string $hookType): bool + { + return $this->propertyReflection->hasHook($hookType); + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + return $this->propertyReflection->getHook($hookType); + } + + public function isProtectedSet(): bool + { + return $this->propertyReflection->isProtectedSet(); + } + + public function isPrivateSet(): bool + { + return $this->propertyReflection->isPrivateSet(); + } + + public function getAttributes(): array + { + return $this->propertyReflection->getAttributes(); + } + + public function getReadableType(): Type + { + return $this->propertyReflection->getReadableType(); + } + + public function getWritableType(): Type + { + return $this->propertyReflection->getWritableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return $this->propertyReflection->canChangeTypeAfterAssignment(); + } + + public function isReadable(): bool + { + return $this->propertyReflection->isReadable(); + } + + public function isWritable(): bool + { + return $this->propertyReflection->isWritable(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->propertyReflection->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->propertyReflection->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->propertyReflection->isInternal(); + } + + public function isDummy(): TrinaryLogic + { + return $this->propertyReflection->isDummy(); + } + +} diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index 8ca22854e0..e42e6ccc52 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -6,23 +6,33 @@ use PHPStan\Analyser\Scope; /** + * This is the interface custom rules implement. To register it in the configuration file + * use the `phpstan.rules.rule` service tag: + * + * ``` + * services: + * - + * class: App\MyRule + * tags: + * - phpstan.rules.rule + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/rules + * * @api - * @phpstan-template TNodeType of \PhpParser\Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string - * @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 e309449564..c8547fa4fd 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,8 +2,21 @@ namespace PHPStan\Rules; -/** @api */ -class RuleErrorBuilder +use PhpParser\Node; +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; @@ -13,12 +26,16 @@ class RuleErrorBuilder private const TYPE_IDENTIFIER = 16; private const TYPE_METADATA = 32; private const TYPE_NON_IGNORABLE = 64; + private const TYPE_FIXABLE_NODE = 128; private int $type; /** @var mixed[] */ private array $properties; + /** @var list */ + private array $tips = []; + private function __construct(string $message) { $this->properties['message'] = $message; @@ -26,61 +43,110 @@ private function __construct(string $message) } /** - * @return array + * @return array}> */ public static function getRuleErrorTypes(): array { return [ self::TYPE_MESSAGE => [ RuleError::class, - 'message', - 'string', - 'string', + [ + [ + 'message', // property name + 'string', // native type + 'string', // PHPDoc type + ], + ], ], 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, + [], + ], + self::TYPE_FIXABLE_NODE => [ + FixableNodeRuleError::class, + [ + [ + 'originalNode', + '\PhpParser\Node', + '\PhpParser\Node', + ], + [ + 'newNodeCallable', + null, + 'callable(\PhpParser\Node): \PhpParser\Node', + ], + ], ], ]; } + /** + * @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; @@ -89,29 +155,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; @@ -120,6 +249,8 @@ public function identifier(string $identifier): self /** * @param mixed[] $metadata + * @phpstan-this-out self + * @return self */ public function metadata(array $metadata): self { @@ -129,6 +260,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; @@ -136,12 +271,32 @@ public function nonIgnorable(): self return $this; } + /** + * @internal Experimental + * @template TNode of Node + * @param TNode $node + * @param callable(TNode): Node $cb + * @phpstan-this-out self + * @return self + */ + public function fixNode(Node $node, callable $cb): self + { + $this->properties['originalNode'] = $node; + $this->properties['newNodeCallable'] = $cb; + $this->type |= self::TYPE_FIXABLE_NODE; + + 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(); @@ -149,6 +304,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/RuleError129.php b/src/Rules/RuleErrors/RuleError129.php new file mode 100644 index 0000000000..bd003c4f5a --- /dev/null +++ b/src/Rules/RuleErrors/RuleError129.php @@ -0,0 +1,40 @@ +message; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError131.php b/src/Rules/RuleErrors/RuleError131.php new file mode 100644 index 0000000000..ba607b957e --- /dev/null +++ b/src/Rules/RuleErrors/RuleError131.php @@ -0,0 +1,48 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError133.php b/src/Rules/RuleErrors/RuleError133.php new file mode 100644 index 0000000000..481c6f0bcf --- /dev/null +++ b/src/Rules/RuleErrors/RuleError133.php @@ -0,0 +1,55 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError135.php b/src/Rules/RuleErrors/RuleError135.php new file mode 100644 index 0000000000..11f52de24a --- /dev/null +++ b/src/Rules/RuleErrors/RuleError135.php @@ -0,0 +1,63 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError137.php b/src/Rules/RuleErrors/RuleError137.php new file mode 100644 index 0000000000..47925389f3 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError137.php @@ -0,0 +1,48 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError139.php b/src/Rules/RuleErrors/RuleError139.php new file mode 100644 index 0000000000..d40a1721d6 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError139.php @@ -0,0 +1,56 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError141.php b/src/Rules/RuleErrors/RuleError141.php new file mode 100644 index 0000000000..5713a025ba --- /dev/null +++ b/src/Rules/RuleErrors/RuleError141.php @@ -0,0 +1,63 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError143.php b/src/Rules/RuleErrors/RuleError143.php new file mode 100644 index 0000000000..1a08b2cf53 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError143.php @@ -0,0 +1,71 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError145.php b/src/Rules/RuleErrors/RuleError145.php new file mode 100644 index 0000000000..f23ae4b9e7 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError145.php @@ -0,0 +1,48 @@ +message; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError147.php b/src/Rules/RuleErrors/RuleError147.php new file mode 100644 index 0000000000..47a80d87d8 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError147.php @@ -0,0 +1,56 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError149.php b/src/Rules/RuleErrors/RuleError149.php new file mode 100644 index 0000000000..802779e560 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError149.php @@ -0,0 +1,63 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError151.php b/src/Rules/RuleErrors/RuleError151.php new file mode 100644 index 0000000000..284a5700c5 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError151.php @@ -0,0 +1,71 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError153.php b/src/Rules/RuleErrors/RuleError153.php new file mode 100644 index 0000000000..24052dc8c9 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError153.php @@ -0,0 +1,56 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError155.php b/src/Rules/RuleErrors/RuleError155.php new file mode 100644 index 0000000000..038f19d079 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError155.php @@ -0,0 +1,64 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError157.php b/src/Rules/RuleErrors/RuleError157.php new file mode 100644 index 0000000000..a2a71b9aca --- /dev/null +++ b/src/Rules/RuleErrors/RuleError157.php @@ -0,0 +1,71 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError159.php b/src/Rules/RuleErrors/RuleError159.php new file mode 100644 index 0000000000..638054207c --- /dev/null +++ b/src/Rules/RuleErrors/RuleError159.php @@ -0,0 +1,79 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError161.php b/src/Rules/RuleErrors/RuleError161.php new file mode 100644 index 0000000000..008cfef2de --- /dev/null +++ b/src/Rules/RuleErrors/RuleError161.php @@ -0,0 +1,52 @@ +message; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError163.php b/src/Rules/RuleErrors/RuleError163.php new file mode 100644 index 0000000000..840450cce0 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError163.php @@ -0,0 +1,60 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError165.php b/src/Rules/RuleErrors/RuleError165.php new file mode 100644 index 0000000000..79eb32732d --- /dev/null +++ b/src/Rules/RuleErrors/RuleError165.php @@ -0,0 +1,67 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError167.php b/src/Rules/RuleErrors/RuleError167.php new file mode 100644 index 0000000000..ccc7d2f3e8 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError167.php @@ -0,0 +1,75 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError169.php b/src/Rules/RuleErrors/RuleError169.php new file mode 100644 index 0000000000..7f106fec80 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError169.php @@ -0,0 +1,60 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError171.php b/src/Rules/RuleErrors/RuleError171.php new file mode 100644 index 0000000000..bd01419ffb --- /dev/null +++ b/src/Rules/RuleErrors/RuleError171.php @@ -0,0 +1,68 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError173.php b/src/Rules/RuleErrors/RuleError173.php new file mode 100644 index 0000000000..17e1855459 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError173.php @@ -0,0 +1,75 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError175.php b/src/Rules/RuleErrors/RuleError175.php new file mode 100644 index 0000000000..ed0f4965f7 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError175.php @@ -0,0 +1,83 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError177.php b/src/Rules/RuleErrors/RuleError177.php new file mode 100644 index 0000000000..2acb39c865 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError177.php @@ -0,0 +1,60 @@ +message; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError179.php b/src/Rules/RuleErrors/RuleError179.php new file mode 100644 index 0000000000..128b918743 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError179.php @@ -0,0 +1,68 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError181.php b/src/Rules/RuleErrors/RuleError181.php new file mode 100644 index 0000000000..601aaf38c8 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError181.php @@ -0,0 +1,75 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError183.php b/src/Rules/RuleErrors/RuleError183.php new file mode 100644 index 0000000000..61f5965218 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError183.php @@ -0,0 +1,83 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError185.php b/src/Rules/RuleErrors/RuleError185.php new file mode 100644 index 0000000000..14dd4f7a9d --- /dev/null +++ b/src/Rules/RuleErrors/RuleError185.php @@ -0,0 +1,68 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError187.php b/src/Rules/RuleErrors/RuleError187.php new file mode 100644 index 0000000000..a76455ad71 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError187.php @@ -0,0 +1,76 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError189.php b/src/Rules/RuleErrors/RuleError189.php new file mode 100644 index 0000000000..d6b044c426 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError189.php @@ -0,0 +1,83 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError191.php b/src/Rules/RuleErrors/RuleError191.php new file mode 100644 index 0000000000..55b9f26662 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError191.php @@ -0,0 +1,91 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError193.php b/src/Rules/RuleErrors/RuleError193.php new file mode 100644 index 0000000000..452a3d7195 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError193.php @@ -0,0 +1,41 @@ +message; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError195.php b/src/Rules/RuleErrors/RuleError195.php new file mode 100644 index 0000000000..0c662f1da3 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError195.php @@ -0,0 +1,49 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError197.php b/src/Rules/RuleErrors/RuleError197.php new file mode 100644 index 0000000000..e2b5af378c --- /dev/null +++ b/src/Rules/RuleErrors/RuleError197.php @@ -0,0 +1,56 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError199.php b/src/Rules/RuleErrors/RuleError199.php new file mode 100644 index 0000000000..c9ed6c0901 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError199.php @@ -0,0 +1,64 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError201.php b/src/Rules/RuleErrors/RuleError201.php new file mode 100644 index 0000000000..2ecf6acaad --- /dev/null +++ b/src/Rules/RuleErrors/RuleError201.php @@ -0,0 +1,49 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError203.php b/src/Rules/RuleErrors/RuleError203.php new file mode 100644 index 0000000000..a04fc867b2 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError203.php @@ -0,0 +1,57 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError205.php b/src/Rules/RuleErrors/RuleError205.php new file mode 100644 index 0000000000..697c9eb673 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError205.php @@ -0,0 +1,64 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError207.php b/src/Rules/RuleErrors/RuleError207.php new file mode 100644 index 0000000000..8ee9144bc4 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError207.php @@ -0,0 +1,72 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError209.php b/src/Rules/RuleErrors/RuleError209.php new file mode 100644 index 0000000000..e007d8ed6c --- /dev/null +++ b/src/Rules/RuleErrors/RuleError209.php @@ -0,0 +1,49 @@ +message; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError211.php b/src/Rules/RuleErrors/RuleError211.php new file mode 100644 index 0000000000..6e56086b32 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError211.php @@ -0,0 +1,57 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError213.php b/src/Rules/RuleErrors/RuleError213.php new file mode 100644 index 0000000000..a9e6203b31 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError213.php @@ -0,0 +1,64 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError215.php b/src/Rules/RuleErrors/RuleError215.php new file mode 100644 index 0000000000..01c8202957 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError215.php @@ -0,0 +1,72 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError217.php b/src/Rules/RuleErrors/RuleError217.php new file mode 100644 index 0000000000..e78f445f2b --- /dev/null +++ b/src/Rules/RuleErrors/RuleError217.php @@ -0,0 +1,57 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError219.php b/src/Rules/RuleErrors/RuleError219.php new file mode 100644 index 0000000000..2bdc632dc0 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError219.php @@ -0,0 +1,65 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError221.php b/src/Rules/RuleErrors/RuleError221.php new file mode 100644 index 0000000000..d43a6004fa --- /dev/null +++ b/src/Rules/RuleErrors/RuleError221.php @@ -0,0 +1,72 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError223.php b/src/Rules/RuleErrors/RuleError223.php new file mode 100644 index 0000000000..b3287c9a05 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError223.php @@ -0,0 +1,80 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError225.php b/src/Rules/RuleErrors/RuleError225.php new file mode 100644 index 0000000000..06ef862f28 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError225.php @@ -0,0 +1,53 @@ +message; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError227.php b/src/Rules/RuleErrors/RuleError227.php new file mode 100644 index 0000000000..633cee8a5c --- /dev/null +++ b/src/Rules/RuleErrors/RuleError227.php @@ -0,0 +1,61 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError229.php b/src/Rules/RuleErrors/RuleError229.php new file mode 100644 index 0000000000..42c1706973 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError229.php @@ -0,0 +1,68 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError231.php b/src/Rules/RuleErrors/RuleError231.php new file mode 100644 index 0000000000..bbbdb5a38f --- /dev/null +++ b/src/Rules/RuleErrors/RuleError231.php @@ -0,0 +1,76 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError233.php b/src/Rules/RuleErrors/RuleError233.php new file mode 100644 index 0000000000..b80cc0c36d --- /dev/null +++ b/src/Rules/RuleErrors/RuleError233.php @@ -0,0 +1,61 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError235.php b/src/Rules/RuleErrors/RuleError235.php new file mode 100644 index 0000000000..aaf1c80dee --- /dev/null +++ b/src/Rules/RuleErrors/RuleError235.php @@ -0,0 +1,69 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError237.php b/src/Rules/RuleErrors/RuleError237.php new file mode 100644 index 0000000000..1d4abc4332 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError237.php @@ -0,0 +1,76 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError239.php b/src/Rules/RuleErrors/RuleError239.php new file mode 100644 index 0000000000..5ee0033dc2 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError239.php @@ -0,0 +1,84 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError241.php b/src/Rules/RuleErrors/RuleError241.php new file mode 100644 index 0000000000..65d853915a --- /dev/null +++ b/src/Rules/RuleErrors/RuleError241.php @@ -0,0 +1,61 @@ +message; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError243.php b/src/Rules/RuleErrors/RuleError243.php new file mode 100644 index 0000000000..e5762ee32b --- /dev/null +++ b/src/Rules/RuleErrors/RuleError243.php @@ -0,0 +1,69 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError245.php b/src/Rules/RuleErrors/RuleError245.php new file mode 100644 index 0000000000..369e7cbee3 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError245.php @@ -0,0 +1,76 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError247.php b/src/Rules/RuleErrors/RuleError247.php new file mode 100644 index 0000000000..0a482fa833 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError247.php @@ -0,0 +1,84 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError249.php b/src/Rules/RuleErrors/RuleError249.php new file mode 100644 index 0000000000..ed86576c33 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError249.php @@ -0,0 +1,69 @@ +message; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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/RuleError251.php b/src/Rules/RuleErrors/RuleError251.php new file mode 100644 index 0000000000..b77d8060f8 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError251.php @@ -0,0 +1,77 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError253.php b/src/Rules/RuleErrors/RuleError253.php new file mode 100644 index 0000000000..17fad64bc1 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError253.php @@ -0,0 +1,84 @@ +message; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} diff --git a/src/Rules/RuleErrors/RuleError255.php b/src/Rules/RuleErrors/RuleError255.php new file mode 100644 index 0000000000..61d6fbd505 --- /dev/null +++ b/src/Rules/RuleErrors/RuleError255.php @@ -0,0 +1,92 @@ +message; + } + + public function getLine(): int + { + return $this->line; + } + + public function getFile(): string + { + return $this->file; + } + + public function getFileDescription(): string + { + return $this->fileDescription; + } + + public function getTip(): string + { + return $this->tip; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @return mixed[] + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function getOriginalNode(): Node + { + return $this->originalNode; + } + + /** + * @return callable(Node): Node + */ + public function getNewNodeCallable(): callable + { + return $this->newNodeCallable; + } + +} 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 4a119f416a..1b46ccfbe7 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -3,51 +3,50 @@ namespace PHPStan\Rules; use PhpParser\Node\Expr; -use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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, + #[AutowiredParameter] + private bool $checkNullables, + #[AutowiredParameter] + private bool $checkThisOnly, + #[AutowiredParameter] + private bool $checkUnionTypes, + #[AutowiredParameter] + private bool $checkExplicitMixed, + #[AutowiredParameter] + private bool $checkImplicitMixed, + #[AutowiredParameter] + private bool $checkBenevolentUnionTypes, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkNullables = $checkNullables; - $this->checkThisOnly = $checkThisOnly; - $this->checkUnionTypes = $checkUnionTypes; - $this->checkExplicitMixed = $checkExplicitMixed; } /** @api */ @@ -56,157 +55,283 @@ public function isThis(Expr $expression): bool return $expression instanceof Expr\Variable && $expression->name === 'this'; } - /** @api */ - public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool + private function transformCommonType(Type $type): Type { - if ( - $this->checkExplicitMixed - ) { - $traverse = static function (Type $type, callable $traverse): Type { - if ($type instanceof TemplateMixedType) { + if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { + return $type; + } + + return TypeTraverser::map($type, function (Type $type, callable $traverse) { + if ($type instanceof TemplateMixedType) { + if ($this->checkExplicitMixed) { return $type->toStrictMixedType(); } - if ( - $type instanceof MixedType - && $type->isExplicitMixed() - ) { - return new StrictMixedType(); + } + if ( + $type instanceof MixedType + && ( + ($type->isExplicitMixed() && $this->checkExplicitMixed) + || (!$type->isExplicitMixed() && $this->checkImplicitMixed) + ) + ) { + return new StrictMixedType(); + } + + return $traverse($type); + }); + } + + /** + * @return array{Type, bool} + */ + private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array + { + $checkForUnion = $this->checkUnionTypes; + $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { + if ($acceptedType instanceof CallableType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; } - return $traverse($type); - }; - $acceptingType = TypeTraverser::map($acceptingType, $traverse); - $acceptedType = TypeTraverser::map($acceptedType, $traverse); - } + return new CallableType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), + ); + } - if ( - !$this->checkNullables - && !$acceptingType instanceof NullType - && !$acceptedType instanceof NullType - && !$acceptedType instanceof BenevolentUnionType - ) { - $acceptedType = TypeCombinator::removeNull($acceptedType); - } + if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } - $accepts = $acceptingType->accepts($acceptedType, $strictTypes); - if (!$accepts->yes() && $acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) { - foreach ($acceptingType->getTypes() as $innerType) { - if (self::accepts($innerType, $acceptedType, $strictTypes)) { - return true; + return new ClosureType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getCallSiteVarianceMap(), + $acceptedType->getTemplateTags(), + $acceptedType->getThrowPoints(), + $acceptedType->getImpurePoints(), + $acceptedType->getInvalidateExpressions(), + $acceptedType->getUsedVariables(), + $acceptedType->acceptsNamedArguments(), + $acceptedType->mustUseReturnValue(), + ); + } + + if ( + !$this->checkNullables + && !$acceptingType instanceof NullType + && !$acceptedType instanceof NullType + && !$acceptedType instanceof BenevolentUnionType + ) { + return $traverse(TypeCombinator::removeNull($acceptedType)); + } + + if ($this->checkBenevolentUnionTypes) { + if ($acceptedType instanceof BenevolentUnionType) { + $checkForUnion = true; + return $traverse(TypeUtils::toStrictUnion($acceptedType)); } } - return false; - } + return $traverse($this->transformCommonType($acceptedType)); + }); - if ( - $acceptedType->isArray()->yes() - && $acceptingType->isArray()->yes() - && !$acceptingType->isIterableAtLeastOnce()->yes() - && count(TypeUtils::getConstantArrays($acceptedType)) === 0 - && count(TypeUtils::getConstantArrays($acceptingType)) === 0 - ) { - return self::accepts( - $acceptingType->getIterableKeyType(), - $acceptedType->getIterableKeyType(), - $strictTypes - ) && self::accepts( - $acceptingType->getIterableValueType(), - $acceptedType->getIterableValueType(), - $strictTypes - ); - } + return [$acceptedType, $checkForUnion]; + } - return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); + /** @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 new RuleLevelHelperAcceptsResult( + $checkForUnion ? $accepts->yes() : !$accepts->no(), + $accepts->reasons, + ); } /** * @api - * @param Scope $scope - * @param Expr $var - * @param string $unknownClassErrorPattern * @param callable(Type $type): bool $unionTypeCriteriaCallback - * @return FoundTypeResult */ public function findTypeToCheck( Scope $scope, Expr $var, string $unknownClassErrorPattern, - callable $unionTypeCriteriaCallback + callable $unionTypeCriteriaCallback, ): FoundTypeResult { if ($this->checkThisOnly && !$this->isThis($var)) { - return new FoundTypeResult(new ErrorType(), [], []); + return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); - if (!$this->checkNullables && !$type instanceof NullType) { - $type = \PHPStan\Type\TypeCombinator::removeNull($type); - } - if (TypeCombinator::containsNull($type)) { - $type = $scope->getType(NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($var)); + 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() + && !$unionTypeCriteriaCallback(new NullType()) + ) { + $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'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); + $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); + } + } + + $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, []); + 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 b97c8f81e4..de9c582b33 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -4,19 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; /** * @implements Rule */ -class TooWideArrowFunctionReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideArrowFunctionReturnTypehintRule implements Rule { + public function __construct( + private TooWideTypeCheck $check, + ) + { + } + public function getNodeType(): string { return InArrowFunctionNode::class; @@ -24,37 +29,22 @@ 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 []; } - $returnType = $scope->getType($expr); - if ($returnType instanceof NullType) { + $functionReturnType = $scope->getFunctionType($arrowFunction->returnType, false, false); + if (!$functionReturnType instanceof UnionType) { return []; } - $messages = []; - foreach ($functionReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return type.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) - ))->build(); - } - return $messages; + return $this->check->checkAnonymousFunction($scope->getType($expr), $functionReturnType); } } diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index d70e4c92e5..d95debd14a 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -4,20 +4,26 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; 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; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClosureReturnStatementsNode> + * @implements Rule */ -class TooWideClosureReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideClosureReturnTypehintRule implements Rule { + public function __construct( + private TooWideTypeCheck $check, + ) + { + } + public function getNodeType(): string { return ClosureReturnStatementsNode::class; @@ -25,11 +31,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 +46,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(); @@ -59,24 +65,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $returnType = TypeCombinator::union(...$returnTypes); - if ($returnType instanceof NullType) { - return []; - } - - $messages = []; - foreach ($closureReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return type.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) - ))->build(); - } - - return $messages; + return $this->check->checkAnonymousFunction(TypeCombinator::union(...$returnTypes), $closureReturnType); } } diff --git a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php new file mode 100644 index 0000000000..1acbe9422d --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php @@ -0,0 +1,43 @@ + + */ +#[RegisteredRule(level: 4)] +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->getStartLine(), + $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 3506a48bed..c6e65c0fd5 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -4,22 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; 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 sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\FunctionReturnStatementsNode> + * @implements Rule */ -class TooWideFunctionReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideFunctionReturnTypehintRule implements Rule { + public function __construct( + private TooWideTypeCheck $check, + ) + { + } + public function getNodeType(): string { return FunctionReturnStatementsNode::class; @@ -27,65 +29,19 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); - if (!$functionReturnType instanceof UnionType) { - return []; - } - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return []; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return []; - } - - $returnTypes = []; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (count($returnTypes) === 0) { - return []; - } - - $returnType = TypeCombinator::union(...$returnTypes); - - $messages = []; - foreach ($functionReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { - foreach ($node->getExecutionEnds() as $executionEnd) { - if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { - continue; - } - - continue 2; - } - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() never returns %s so it can be removed from the return type.', + $function = $node->getFunctionReflection(); + + return $this->check->checkFunctionReturnType( + $node, + $function->getNativeReturnType(), + $function->getPhpDocReturnType(), + sprintf( + 'Function %s()', $function->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) - ))->build(); - } - - return $messages; + ), + false, + $scope, + ); } } diff --git a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php new file mode 100644 index 0000000000..f96c13f580 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php @@ -0,0 +1,54 @@ + + */ +#[RegisteredRule(level: 4)] +final class TooWideMethodParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + #[AutowiredParameter(ref: '%checkTooWideParameterOutInProtectedAndPublicMethods%')] + private bool $checkProtectedAndPublicMethods, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inMethod = $node->getMethodReflection(); + + if (!$inMethod->isPrivate()) { + if (!$inMethod->getDeclaringClass()->isFinal() && !$inMethod->isFinal()->yes()) { + if (!$this->checkProtectedAndPublicMethods) { + return []; + } + } + } + + return $this->check->check( + $node->getStartLine(), + $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 0cc7c1d17d..77c5ca7b1f 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -4,28 +4,25 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; 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\UnionType; -use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\MethodReturnStatementsNode> + * @implements Rule */ -class TooWideMethodReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideMethodReturnTypehintRule implements Rule { - private bool $checkProtectedAndPublicMethods; - - public function __construct(bool $checkProtectedAndPublicMethods) + public function __construct( + #[AutowiredParameter(ref: '%checkTooWideReturnTypesInProtectedAndPublicMethods%')] + private bool $checkProtectedAndPublicMethods, + private TooWideTypeCheck $check, + ) { - $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; } public function getNodeType(): string @@ -35,82 +32,35 @@ 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 []; - } - } - - $methodReturnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); - if (!$methodReturnType instanceof UnionType) { - return []; - } - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return []; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return []; - } - - $returnTypes = []; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (count($returnTypes) === 0) { - return []; - } - - $returnType = TypeCombinator::union(...$returnTypes); - if ( - !$method->isPrivate() - && ($returnType instanceof NullType || $returnType instanceof ConstantBooleanType) - && !$isFirstDeclaration - ) { - return []; - } - - $messages = []; - foreach ($methodReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { - foreach ($node->getExecutionEnds() as $executionEnd) { - if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { - continue; - } + if (!$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { + if (!$this->checkProtectedAndPublicMethods) { + return []; + } - continue 2; + if ($isFirstDeclaration) { + return []; } } + } - $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() never returns %s so it can be removed from the return type.', + return $this->check->checkFunctionReturnType( + $node, + $method->getNativeReturnType(), + $method->getPhpDocReturnType(), + sprintf( + 'Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) - ))->build(); - } - - return $messages; + ), + !$isFirstDeclaration && !$method->isPrivate(), + $scope, + ); } } diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php new file mode 100644 index 0000000000..4fd41dc37b --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php @@ -0,0 +1,124 @@ + $executionEnds + * @param list $returnStatements + * @param ExtendedParameterReflection[] $parameters + * @return list + */ + public function check( + int $startLine, + 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($startLine, $finalScope, $functionDescription, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + int $startLine, + Scope $scope, + string $functionDescription, + ExtendedParameterReflection $parameter, + ): array + { + $isParamOutType = true; + $outType = $parameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $parameter->getType(); + } + + $variableExpr = new Variable($parameter->getName()); + $variableType = $scope->getType($variableExpr); + + return $this->tooWideTypeCheck->checkParameterOutType( + $outType, + $variableType, + sprintf( + '%s never assigns %%s to &$%s so it can be removed from the %s.', + $functionDescription, + $parameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + ), + sprintf( + '%s never assigns %%s to &$%s so the %s can be changed to %%s.', + $functionDescription, + $parameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + ), + sprintf( + '%s %%s of %s can be narrowed to %%s.', + $isParamOutType ? 'PHPDoc tag @param-out type' : 'By-ref type', + lcfirst($functionDescription), + ), + $scope, + $startLine, + $isParamOutType ? 'paramOut' : 'parameterByRef', + $isParamOutType ? null : 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php new file mode 100644 index 0000000000..387fbfbdcd --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -0,0 +1,93 @@ + + */ +#[RegisteredRule(level: 4)] +final class TooWidePropertyTypeRule implements Rule +{ + + public function __construct( + private ReadWritePropertiesExtensionProvider $extensionProvider, + private TooWideTypeCheck $check, + ) + { + } + + 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); + + 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; + } + } + + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyName); + $propertyErrors = $this->check->checkProperty( + $property, + $propertyReflection->getDeclaringClass(), + $node->getPropertyAssigns(), + $propertyReflection->getNativeType(), + $propertyReflection->getPhpDocType(), + $propertyDescription, + $scope, + ); + foreach ($propertyErrors as $error) { + $errors[] = $error; + } + } + 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/TooWideTypehints/TooWideTypeCheck.php b/src/Rules/TooWideTypehints/TooWideTypeCheck.php new file mode 100644 index 0000000000..e212530b9f --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideTypeCheck.php @@ -0,0 +1,492 @@ + + */ + public function checkProperty( + ClassPropertyNode $node, + ClassReflection $declaringClass, + array $propertyAssigns, + Type $nativePropertyType, + Type $phpDocPropertyType, + string $propertyDescription, + Scope $scope, + ): array + { + $errors = []; + + $assignedTypes = []; + foreach ($propertyAssigns as $assign) { + $assignNode = $assign->getAssign(); + $assignPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($assignNode->getPropertyFetch(), $assign->getScope()); + foreach ($assignPropertyReflections as $assignPropertyReflection) { + if ($node->getName() !== $assignPropertyReflection->getName()) { + continue; + } + if ($declaringClass->getName() !== $assignPropertyReflection->getDeclaringClass()->getName()) { + continue; + } + + $assignedTypes[] = $assignPropertyReflection->getScope()->getType($assignNode->getAssignedExpr()); + } + } + + if ($node->getDefault() !== null) { + $assignedTypes[] = $scope->getType($node->getDefault()); + } + + if ($node->getNativeType() === null && $node->getDefault() === null) { + $assignedTypes[] = new NullType(); + } + + if (count($assignedTypes) === 0) { + return []; + } + + $assignedType = TypeCombinator::union(...$assignedTypes); + + $unionMessagePattern = '%s (%s) is never assigned %%s so it can be removed from the property type.'; + $boolMessagePattern = '%s (%s) is never assigned %%s so the property type can be changed to %%s.'; + + if (!$phpDocPropertyType instanceof MixedType || $phpDocPropertyType->isExplicitMixed()) { + $phpDocPropertyType = TypeUtils::resolveLateResolvableTypes(TypehintHelper::decideType($nativePropertyType, $phpDocPropertyType)); + $narrowedPhpDocType = $this->narrowType($phpDocPropertyType, $assignedType, $scope, false, false); + if (!$narrowedPhpDocType->equals($phpDocPropertyType)) { + $phpDocPropertyTypeDescription = $phpDocPropertyType->describe(VerbosityLevel::getRecommendedLevelByType($phpDocPropertyType)); + return $this->createErrors( + $narrowedPhpDocType, + $phpDocPropertyType, + sprintf($unionMessagePattern, $propertyDescription, $phpDocPropertyTypeDescription), + sprintf($boolMessagePattern, $propertyDescription, $phpDocPropertyTypeDescription), + $node->getStartLine(), + 'property', + null, + ); + } + + if (!$this->reportNestedTooWideType) { + return []; + } + + $narrowedPhpDocType = SimultaneousTypeTraverser::map($phpDocPropertyType, $assignedType, function (Type $declaredType, Type $actualType, callable $traverse) use ($scope) { + $narrowed = $this->narrowType($declaredType, $actualType, $scope, false, false); + if (!$narrowed->equals($declaredType)) { + return $narrowed; + } + return $traverse($declaredType, $actualType); + }); + if ($narrowedPhpDocType->equals($phpDocPropertyType)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Type %s of %s can be narrowed to %s.', + $phpDocPropertyType->describe(VerbosityLevel::getRecommendedLevelByType($phpDocPropertyType)), + lcfirst($propertyDescription), + $narrowedPhpDocType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedPhpDocType)), + ))->identifier('property.nestedUnusedType') + ->line($node->getStartLine()) + ->acceptsReasonsTip($narrowedPhpDocType->accepts($phpDocPropertyType, true)->reasons) + ->build(), + ]; + } + + $narrowedNativeType = $this->narrowType($nativePropertyType, $assignedType, $scope, false, true); + if (!$narrowedNativeType->equals($nativePropertyType)) { + $propertyTypeDescription = $nativePropertyType->describe(VerbosityLevel::getRecommendedLevelByType($nativePropertyType)); + return $this->createErrors( + $narrowedNativeType, + $nativePropertyType, + sprintf($unionMessagePattern, $propertyDescription, $propertyTypeDescription), + sprintf($boolMessagePattern, $propertyDescription, $propertyTypeDescription), + $node->getStartLine(), + 'property', + null, + ); + } + + return $errors; + } + + /** + * @return list + */ + public function checkFunctionReturnType( + MethodReturnStatementsNode|FunctionReturnStatementsNode $node, + Type $nativeFunctionReturnType, + Type $phpDocFunctionReturnType, + string $functionDescription, + bool $checkDescendantClass, + Scope $scope, + ): array + { + $statementResult = $node->getStatementResult(); + if ($statementResult->hasYield()) { + return []; + } + + $returnStatements = $node->getReturnStatements(); + if (count($returnStatements) === 0) { + return []; + } + + $returnTypes = []; + foreach ($returnStatements as $returnStatement) { + $returnNode = $returnStatement->getReturnNode(); + if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); + continue; + } + + $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); + } + + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); + } + + if (!$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + $returnTypes[] = new NullType(); + break; + } + } + + $returnType = TypeCombinator::union(...$returnTypes); + + $unionMessagePattern = sprintf('%s never returns %%s so it can be removed from the return type.', $functionDescription); + $boolMessagePattern = sprintf('%s never returns %%s so the return type can be changed to %%s.', $functionDescription); + + // Do not require to have @return null/true/false in descendant classes + if ( + $checkDescendantClass + && ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) + ) { + return []; + } + + if (!$phpDocFunctionReturnType instanceof MixedType || $phpDocFunctionReturnType->isExplicitMixed()) { + $phpDocFunctionReturnType = TypeUtils::resolveLateResolvableTypes(TypehintHelper::decideType($nativeFunctionReturnType, $phpDocFunctionReturnType)); + + $narrowedPhpDocType = $this->narrowType($phpDocFunctionReturnType, $returnType, $scope, false, false); + if (!$narrowedPhpDocType->equals($phpDocFunctionReturnType)) { + return $this->createErrors( + $narrowedPhpDocType, + $phpDocFunctionReturnType, + $unionMessagePattern, + $boolMessagePattern, + $node->getStartLine(), + 'return', + null, + ); + } + + if (!$this->reportNestedTooWideType) { + return []; + } + + $narrowedPhpDocType = SimultaneousTypeTraverser::map($phpDocFunctionReturnType, $returnType, function (Type $declaredType, Type $actualReturnType, callable $traverse) use ($scope, $checkDescendantClass) { + $narrowed = $this->narrowType($declaredType, $actualReturnType, $scope, $checkDescendantClass, false); + if (!$narrowed->equals($declaredType)) { + return $narrowed; + } + return $traverse($declaredType, $actualReturnType); + }); + if ($narrowedPhpDocType->equals($phpDocFunctionReturnType)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Return type %s of %s can be narrowed to %s.', + $phpDocFunctionReturnType->describe(VerbosityLevel::getRecommendedLevelByType($phpDocFunctionReturnType)), + lcfirst($functionDescription), + $narrowedPhpDocType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedPhpDocType)), + ))->identifier('return.nestedUnusedType') + ->line($node->getStartLine()) + ->acceptsReasonsTip($narrowedPhpDocType->accepts($phpDocFunctionReturnType, true)->reasons) + ->build(), + ]; + } + + $narrowedNativeType = $this->narrowType($nativeFunctionReturnType, $returnType, $scope, false, true); + if (!$narrowedNativeType->equals($nativeFunctionReturnType)) { + return $this->createErrors( + $narrowedNativeType, + $nativeFunctionReturnType, + $unionMessagePattern, + $boolMessagePattern, + $node->getStartLine(), + 'return', + null, + ); + } + + return []; + } + + /** + * @param 'paramOut'|'parameterByRef' $identifierPart + * @return list + */ + public function checkParameterOutType( + Type $parameterOutType, + Type $actualVariableType, + string $unionMessagePattern, + string $boolMessagePattern, + string $nestedTooWideTypePattern, + Scope $scope, + int $startLine, + string $identifierPart, + ?string $tip, + ): array + { + $parameterOutType = TypeUtils::resolveLateResolvableTypes($parameterOutType); + $narrowedType = $this->narrowType($parameterOutType, $actualVariableType, $scope, false, false); + if ($narrowedType->equals($parameterOutType)) { + if (!$this->reportNestedTooWideType) { + return []; + } + + $narrowedType = SimultaneousTypeTraverser::map($parameterOutType, $actualVariableType, function (Type $declaredType, Type $actualType, callable $traverse) use ($scope) { + $narrowed = $this->narrowType($declaredType, $actualType, $scope, false, false); + if (!$narrowed->equals($declaredType)) { + return $narrowed; + } + return $traverse($declaredType, $actualType); + }); + if ($narrowedType->equals($parameterOutType)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + $nestedTooWideTypePattern, + $parameterOutType->describe(VerbosityLevel::getRecommendedLevelByType($parameterOutType)), + $narrowedType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedType)), + ))->identifier(sprintf('%s.nestedUnusedType', $identifierPart)) + ->line($startLine) + ->acceptsReasonsTip($narrowedType->accepts($parameterOutType, true)->reasons) + ->build(), + ]; + } + + return $this->createErrors( + $narrowedType, + $parameterOutType, + $unionMessagePattern, + $boolMessagePattern, + $startLine, + $identifierPart, + $tip, + ); + } + + /** + * @param 'return'|'property'|'paramOut'|'parameterByRef' $identifierPart + * @return list + */ + private function createErrors( + Type $narrowedType, + Type $originalType, + string $unionMessagePattern, + string $boolMessagePattern, + int $startLine, + string $identifierPart, + ?string $tip, + ): array + { + if ($originalType->isBoolean()->yes()) { + $neverReturns = $narrowedType->isTrue()->yes() ? new ConstantBooleanType(false) : new ConstantBooleanType(true); + + $errorBuilder = RuleErrorBuilder::message(sprintf( + $boolMessagePattern, + $neverReturns->describe(VerbosityLevel::getRecommendedLevelByType($neverReturns)), + $narrowedType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedType)), + ))->identifier(sprintf('%s.tooWideBool', $identifierPart))->line($startLine); + if ($tip !== null) { + $errorBuilder->tip($tip); + } + + return [$errorBuilder->build()]; + } + + if (!$originalType instanceof UnionType) { + return []; + } + + $messages = []; + foreach ($originalType->getTypes() as $innerType) { + if (!$narrowedType->isSuperTypeOf($innerType)->no()) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( + $unionMessagePattern, + $innerType->describe(VerbosityLevel::getRecommendedLevelByType($innerType)), + ))->identifier(sprintf('%s.unusedType', $identifierPart))->line($startLine); + if ($tip !== null) { + $errorBuilder->tip($tip); + } + + $messages[] = $errorBuilder->build(); + } + + return $messages; + } + + /** + * @return list + */ + public function checkAnonymousFunction( + Type $returnType, + UnionType $functionReturnType, + ): array + { + if ($returnType->isNull()->yes()) { + return []; + } + $messages = []; + foreach ($functionReturnType->getTypes() as $type) { + if (!$type->isSuperTypeOf($returnType)->no()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + '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; + } + + private function narrowType( + Type $declaredType, + Type $actualReturnType, + Scope $scope, + bool $checkDescendantClass, + bool $native, + ): Type + { + if ($declaredType instanceof UnionType) { + if ( + $declaredType->isConstantScalarValue()->yes() + && $actualReturnType->isConstantScalarValue()->yes() + ) { + return $declaredType; + } + $usedTypes = []; + foreach ($declaredType->getTypes() as $innerType) { + if ($innerType->isSuperTypeOf($actualReturnType)->no()) { + if (!$checkDescendantClass) { + continue; + } + if ($innerType->isNull()->yes()) { + $usedTypes[] = $innerType; + continue; + } + if ( + !$actualReturnType->isTrue()->yes() + && !$actualReturnType->isFalse()->yes() + && !$actualReturnType->isNull()->yes() + ) { + continue; + } + } + + $usedTypes[] = $innerType; + } + + return TypeCombinator::union(...$usedTypes); + } + + if (!$this->reportTooWideBool) { + return $declaredType; + } + + if ($native && !$scope->getPhpVersion()->supportsTrueAndFalseStandaloneType()->yes()) { + return $declaredType; + } + + if (!$declaredType->isBoolean()->yes()) { + return $declaredType; + } + + if ( + $declaredType->isTrue()->yes() + || $declaredType->isFalse()->yes() + ) { + return $declaredType; + } + + $usedTypes = []; + foreach ($declaredType->getFiniteTypes() as $innerType) { + if ($innerType->isSuperTypeOf($actualReturnType)->no()) { + if (!$checkDescendantClass) { + continue; + } + if ( + !$actualReturnType->isTrue()->yes() + && !$actualReturnType->isFalse()->yes() + && !$actualReturnType->isNull()->yes() + ) { + continue; + } + } + + $usedTypes[] = $innerType; + } + + return TypeCombinator::union(...$usedTypes); + } + +} diff --git a/src/Rules/Traits/ConflictingTraitConstantsRule.php b/src/Rules/Traits/ConflictingTraitConstantsRule.php new file mode 100644 index 0000000000..63b1d047a2 --- /dev/null +++ b/src/Rules/Traits/ConflictingTraitConstantsRule.php @@ -0,0 +1,254 @@ + + */ +#[RegisteredRule(level: 0)] +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, selfClass: $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, selfClass: $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..d2d146f67f --- /dev/null +++ b/src/Rules/Traits/ConstantsInTraitsRule.php @@ -0,0 +1,48 @@ + + */ +#[RegisteredRule(level: 0)] +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..1cb97f91d2 --- /dev/null +++ b/src/Rules/Traits/NotAnalysedTraitRule.php @@ -0,0 +1,66 @@ + + */ +#[RegisteredRule(level: 4)] +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..d2601de8ad --- /dev/null +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -0,0 +1,53 @@ + + */ +#[RegisteredRule(level: 0)] +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..ee430f7560 --- /dev/null +++ b/src/Rules/Traits/TraitDeclarationCollector.php @@ -0,0 +1,31 @@ + + */ +#[RegisteredCollector(level: 4)] +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..3d5976f920 --- /dev/null +++ b/src/Rules/Traits/TraitUseCollector.php @@ -0,0 +1,32 @@ +> + */ +#[RegisteredCollector(level: 4)] +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..cad8a265f9 --- /dev/null +++ b/src/Rules/Types/InvalidTypesInUnionRule.php @@ -0,0 +1,127 @@ + + */ +#[RegisteredRule(level: 0)] +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 856a8c39de..9e36e01387 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,59 +3,70 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\ShouldNotHappenException; +use function array_merge; +use function in_array; +use function is_array; +use function is_string; +use function sprintf; -class UnusedFunctionParametersCheck +#[AutowiredService] +final class UnusedFunctionParametersCheck { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%featureToggles.reportPreciseLineForUnusedFunctionParameter%')] + private bool $reportExactLine, + ) { - $this->reflectionProvider = $reflectionProvider; } /** - * @param \PHPStan\Analyser\Scope $scope - * @param string[] $parameterNames - * @param \PhpParser\Node[] $statements - * @param string $unusedParameterMessage - * @param string $identifier - * @param mixed[] $additionalMetadata - * @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, - array $additionalMetadata ): array { - $unusedParameters = array_fill_keys($parameterNames, true); - foreach ($this->getUsedVariables($scope, $statements) as $variableName) { - if (!isset($unusedParameters[$variableName])) { - continue; + $unusedParameters = []; + foreach ($parameterVars as $variable) { + if (!is_string($variable->name)) { + throw new ShouldNotHappenException(); } + $unusedParameters[$variable->name] = $variable; + } + foreach ($this->getUsedVariables($scope, $statements) as $variableName) { unset($unusedParameters[$variableName]); } + $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name) - )->identifier($identifier)->metadata($additionalMetadata + ['variableName' => $name])->build(); + foreach ($unusedParameters as $name => $variable) { + $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $name))->identifier($identifier); + if ($this->reportExactLine) { + $errorBuilder->line($variable->getStartLine()); + } + $errors[] = $errorBuilder->build(); } return $errors; } /** - * @param \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 @@ -64,14 +75,14 @@ private function getUsedVariables(Scope $scope, $node): array if ($node instanceof Node) { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if ($functionName === 'func_get_args') { + if (in_array($functionName, ['func_get_args', 'get_defined_vars'], true)) { return $scope->getDefinedVariables(); } } - if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') { + if ($node instanceof Variable && is_string($node->name) && $node->name !== 'this') { return [$node->name]; } - if ($node instanceof Node\Expr\ClosureUse && is_string($node->var->name)) { + if ($node instanceof Node\ClosureUse && is_string($node->var->name)) { return [$node->var->name]; } if ( @@ -81,11 +92,9 @@ private function getUsedVariables(Scope $scope, $node): array ) { 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 00dea2da1c..fb6c42086b 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -4,23 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantArrayType; 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 */ +#[RegisteredRule(level: 0)] final class CompactVariablesRule implements Rule { - private bool $checkMaybeUndefinedVariables; - - public function __construct(bool $checkMaybeUndefinedVariables) + public function __construct( + #[AutowiredParameter] + private bool $checkMaybeUndefinedVariables, + ) { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -52,12 +58,12 @@ public function processNode(Node $node, Scope $scope): array if ($scopeHasVariable->no()) { $messages[] = RuleErrorBuilder::message( - sprintf('Call to function compact() contains undefined variable $%s.', $variableName) - )->line($argument->getLine())->build(); + 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) - )->line($argument->getLine())->build(); + sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName), + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); } } } @@ -66,26 +72,24 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Type $type - * @return array + * @return list */ private function findConstantStrings(Type $type): array { - if ($type instanceof ConstantStringType) { - return [$type]; + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) > 0) { + return $constantStrings; } - if ($type instanceof ConstantArrayType) { - $result = []; - foreach ($type->getValueTypes() as $valueType) { + $result = []; + foreach ($type->getConstantArrays() as $constantArrayType) { + foreach ($constantArrayType->getValueTypes() as $valueType) { $constantStrings = $this->findConstantStrings($valueType); $result = array_merge($result, $constantStrings); } - - return $result; } - return []; + return $result; } } diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index f50735792d..8fbb1e5d0b 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,27 +3,34 @@ 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\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +final class DefinedVariableRule implements Rule { - private bool $cliArgumentsVariablesRegistered; - - private bool $checkMaybeUndefinedVariables; - public function __construct( - bool $cliArgumentsVariablesRegistered, - bool $checkMaybeUndefinedVariables + #[AutowiredParameter] + private bool $cliArgumentsVariablesRegistered, + #[AutowiredParameter] + private bool $checkMaybeUndefinedVariables, ) { - $this->cliArgumentsVariablesRegistered = $cliArgumentsVariablesRegistered; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -33,11 +40,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))); + } + } + + foreach ($variableNameScopes as $name => $variableScope) { + $errors = array_merge($errors, $this->processSingleVariable( + $variableScope, + $node, + (string) $name, // @phpstan-ignore cast.useless + )); } - if ($this->cliArgumentsVariablesRegistered && in_array($node->name, [ + return $errors; + } + + /** + * @return list + */ + private function processSingleVariable(Scope $scope, Variable $node, string $variableName): array + { + if ($this->cliArgumentsVariablesRegistered && in_array($variableName, [ 'argc', 'argv', ], true)) { @@ -47,41 +78,23 @@ public function processNode(Node $node, Scope $scope): array } } - if ($scope->isInExpressionAssign($node)) { + if ($scope->isInExpressionAssign($node) || $scope->isUndefinedExpressionAllowed($node)) { return []; } - if ($scope->hasVariableType($node->name)->no()) { + if ($scope->hasVariableType($variableName)->no()) { return [ - RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->name)) + RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $variableName)) ->identifier('variable.undefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) ->build(), ]; } elseif ( $this->checkMaybeUndefinedVariables - && !$scope->hasVariableType($node->name)->yes() + && !$scope->hasVariableType($variableName)->yes() ) { return [ - RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->name)) - ->identifier('variable.maybeUndefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) + RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName)) + ->identifier('variable.undefined') ->build(), ]; } @@ -89,20 +102,4 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** - * @param Scope $scope - * @return array> - */ - private function getParentVariables(Scope $scope): array - { - $variables = []; - $parent = $scope->getParentScope(); - while ($parent !== null) { - $variables[] = $parent->getDefinedVariables(); - $parent = $parent->getParentScope(); - } - - return $variables; - } - } diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php index ab9ce3c38d..d1656a3be2 100644 --- a/src/Rules/Variables/EmptyRule.php +++ b/src/Rules/Variables/EmptyRule.php @@ -4,22 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\NullType; +use PHPStan\Rules\Rule; use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class EmptyRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 1)] +final class EmptyRule implements Rule { - private IssetCheck $issetCheck; - - public function __construct(IssetCheck $issetCheck) + public function __construct(private IssetCheck $issetCheck) { - $this->issetCheck = $issetCheck; } public function getNodeType(): string @@ -29,12 +27,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); - $isFalsey = (new ConstantBooleanType(false))->isSuperTypeOf($type->toBoolean()); + $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', 'empty', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } + $isFalsey = $type->toBoolean()->isFalse(); if ($isFalsey->maybe()) { return null; } diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index c9654bd63d..839241a169 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -4,21 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; -use PHPStan\Type\NullType; +use PHPStan\Rules\Rule; use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class IssetRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 1)] +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 @@ -30,8 +29,8 @@ public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->vars as $var) { - $error = $this->issetCheck->check($var, $scope, 'in isset()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); + $error = $this->issetCheck->check($var, $scope, 'in isset()', 'isset', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 144e07932c..79941d0293 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -4,32 +4,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; -use PHPStan\Type\NullType; +use PHPStan\Rules\Rule; use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class NullCoalesceRule implements \PHPStan\Rules\Rule +#[RegisteredRule(level: 1)] +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 = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -42,9 +41,9 @@ public function processNode(Node $node, Scope $scope): array }; if ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', $typeMessageCallback); + $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback); } elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { - $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', $typeMessageCallback); + $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', 'nullCoalesce', $typeMessageCallback); } else { return []; } diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php new file mode 100644 index 0000000000..5bc0118057 --- /dev/null +++ b/src/Rules/Variables/ParameterOutAssignedTypeRule.php @@ -0,0 +1,122 @@ + + */ +#[RegisteredRule(level: 3)] +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..edddff8a91 --- /dev/null +++ b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php @@ -0,0 +1,134 @@ + + */ +#[RegisteredRule(level: 3)] +final class ParameterOutExecutionEndTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return ExecutionEndNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $endNode = $node->getNode(); + if ($endNode instanceof Node\Stmt\Expression) { + $endNodeExpr = $endNode->expr; + $endNodeExprType = $scope->getType($endNodeExpr); + if ($endNodeExprType instanceof NeverType && $endNodeExprType->isExplicit()) { + return []; + } + } + + $parameters = $inFunction->getParameters(); + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($scope, $inFunction, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + FunctionReflection|ExtendedMethodReflection $inFunction, + ExtendedParameterReflection $parameter, + ): array + { + $outType = $parameter->getOutType(); + if ($outType === null) { + return []; + } + + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($parameter->getName()))->no()) { + return []; + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $variableExpr = new Node\Expr\Variable($parameter->getName()); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $variableExpr, + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($variableExpr); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s @param-out type of %s expects %s, %s given.', + $parameter->getName(), + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('paramOut.type')); + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php deleted file mode 100644 index 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..d2893e17fe 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,16 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 0)] +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 +39,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 +111,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 +132,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/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 5caa3587f4..bab0f8d995 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -6,23 +6,25 @@ use PhpParser\Node\Expr\Clone_; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +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 +#[RegisteredRule(level: 3)] +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 +38,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 +53,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 index 8829390f20..ee067b1ceb 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -3,17 +3,23 @@ namespace PHPStan\Rules\Whitespace; use Nette\Utils\Strings; +use Override; use PhpParser\Node; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; +use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FileNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; /** * @implements Rule */ -class FileWhitespaceRule implements Rule +#[RegisteredRule(level: 0)] +final class FileWhitespaceRule implements Rule { public function getNodeType(): string @@ -31,19 +37,21 @@ public function processNode(Node $node, Scope $scope): array $firstNode = $nodes[0]; $messages = []; if ($firstNode instanceof Node\Stmt\InlineHTML && $firstNode->value === "\xef\xbb\xbf") { - $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.')->build(); + $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.') + ->identifier('whitespace.bom') + ->build(); } $nodeTraverser = new NodeTraverser(); - $visitor = new class () extends \PhpParser\NodeVisitorAbstract { + $visitor = new class () extends NodeVisitorAbstract { - /** @var \PhpParser\Node[] */ - private $lastNodes = []; + /** @var Node[] */ + private array $lastNodes = []; /** - * @param Node $node - * @return int|Node|null + * @return int|null */ + #[Override] public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Declare_) { @@ -58,7 +66,7 @@ public function enterNode(Node $node) } return null; } - return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } /** @@ -80,7 +88,9 @@ public function getLastNodes(): array continue; } - $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine())->build(); + $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine()) + ->identifier('whitespace.fileEnd') + ->build(); } return $messages; diff --git a/src/ShouldNotHappenException.php b/src/ShouldNotHappenException.php index e6317a757d..4f597f4b32 100644 --- a/src/ShouldNotHappenException.php +++ b/src/ShouldNotHappenException.php @@ -2,7 +2,9 @@ namespace PHPStan; -final class ShouldNotHappenException extends \Exception +use Exception; + +final class ShouldNotHappenException extends Exception { /** @api */ diff --git a/src/Testing/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 8a3dbd8214..418e54b533 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -8,70 +8,107 @@ 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\PHPStanTestCase +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()); - - $contents = stream_get_contents($this->getOutputStream()->getStream()); - if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); - } + rewind($this->getOutputStream($decorated, $verbose)->getStream()); + $contents = stream_get_contents($this->getOutputStream($decorated, $verbose)->getStream()); return $this->rtrimMultiline($contents); } - protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): AnalysisResult + /** + * @param array{int, int}|int $numFileErrors + */ + protected function getAnalysisResult(array|int $numFileErrors, int $numGenericErrors): AnalysisResult { - if ($numFileErrors > 5 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { - throw new \PHPStan\ShouldNotHappenException(); + if (is_int($numFileErrors)) { + $offsetFileErrors = 0; + } else { + [$offsetFileErrors, $numFileErrors] = $numFileErrors; + } + + if (!in_array($numFileErrors, range(0, 7), true) || + !in_array($offsetFileErrors, range(0, 7), true) || + !in_array($numGenericErrors, range(0, 2), true) + ) { + throw new ShouldNotHappenException(); } $fileErrors = array_slice([ new Error('Foo', self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 4), - new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), - new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5), + new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), + new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5, tip: 'a tip'), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 2), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', null), - ], 0, $numFileErrors); + new Error('Foobar\\Buz', self::DIRECTORY_PATH . '/foo.php', 5, tip: 'a tip', identifier: 'foobar.buz'), + new Error('Error with @param or @phpstan-param and class@anonymous in the message.', self::DIRECTORY_PATH . '/bar.php', 5), + ], $offsetFileErrors, $numFileErrors); $genericErrors = array_slice([ 'first generic error', - 'second generic error', + 'second generic', ], 0, $numGenericErrors); return new AnalysisResult( @@ -79,17 +116,19 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): $genericErrors, [], [], + [], false, null, - true + 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 e30926172a..fa0eab7747 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -2,17 +2,36 @@ namespace PHPStan\Testing; +use Nette\Utils\Json; +use Nette\Utils\JsonException; use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; +use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\DataProvider; +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 \PHPUnit\Framework\TestCase +abstract class LevelsTestCase extends TestCase { /** * @return array> */ - abstract public function dataTopics(): array; + abstract public static function dataTopics(): array; abstract public function getDataPath(): string; @@ -30,12 +49,9 @@ protected function shouldAutoloadAnalysedFile(): bool return true; } - /** - * @dataProvider dataTopics - * @param string $topic - */ + #[DataProvider('dataTopics')] public function testLevels( - string $topic + string $topic, ): void { $file = sprintf('%s' . DIRECTORY_SEPARATOR . '%s.php', $this->getDataPath(), $topic); @@ -47,20 +63,23 @@ public function testLevels( $exceptions = []; - foreach (range(0, 9) as $level) { + exec(sprintf('%s %s clear-result-cache %s 2>&1', escapeshellarg(PHP_BINARY), $command, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode); + if ($clearResultCacheExitCode !== 0) { + throw new ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); + } + + putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1'); + + foreach (range(0, 10) as $level) { unset($outputLines); - exec(sprintf('%s %s clear-result-cache %s 2>&1', escapeshellarg(PHP_BINARY), $command, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode); - if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); - } exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', $this->shouldAutoloadAnalysedFile() ? sprintf('--autoload-file %s', escapeshellarg($file)) : '', escapeshellarg($file)), $outputLines); $output = implode("\n", $outputLines); try { - $actualJson = \Nette\Utils\Json::decode($output, \Nette\Utils\Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { - throw new \Nette\Utils\JsonException(sprintf('Cannot decode: %s', $output)); + $actualJson = Json::decode($output, Json::FORCE_ARRAY); + } catch (JsonException) { + throw new JsonException(sprintf('Cannot decode: %s', $output)); } if (count($actualJson['files']) > 0) { $normalizedFilePath = $fileHelper->normalizePath($file); @@ -93,6 +112,9 @@ public function testLevels( } } + unset($message['tip']); + unset($message['identifier']); + $messages[] = $message; } @@ -107,6 +129,8 @@ public function testLevels( } } + unset($previousMessage['tip']); + $missingMessages[] = $previousMessage; } @@ -141,30 +165,28 @@ public function getAdditionalAnalysedFiles(): array } /** - * @param string $expectedJsonFile * @param string[] $expectedMessages - * @return \PHPUnit\Framework\AssertionFailedError|null */ - private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?\PHPUnit\Framework\AssertionFailedError + private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?AssertionFailedError { if (count($expectedMessages) === 0) { try { - self::assertFileDoesNotExist($expectedJsonFile); + 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; } @@ -172,8 +194,9 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) return null; } - public static function assertFileDoesNotExist(string $filename, string $message = ''): void + public static function ourCustomAssertFileDoesNotExist(string $filename, string $message = ''): void { + // this method is no longer called assertFileDoesNotExist because this method is final in PHPUnit 10 if (!method_exists(parent::class, 'assertFileDoesNotExist')) { parent::assertFileNotExists($filename, $message); return; diff --git a/src/Testing/NonexistentAnalysedClassRule.php b/src/Testing/NonexistentAnalysedClassRule.php new file mode 100644 index 0000000000..25c7a11459 --- /dev/null +++ b/src/Testing/NonexistentAnalysedClassRule.php @@ -0,0 +1,47 @@ + + */ +final class NonexistentAnalysedClassRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $className = $node->getClassReflection()->getName(); + if ($this->reflectionProvider->hasClass($className)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + '%s %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $node->getClassReflection()->getClassTypeDescription(), + $node->getClassReflection()->getName(), + )) + ->identifier('phpstan.classNotFound') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Testing/NonexistentAnalysedTraitRule.php b/src/Testing/NonexistentAnalysedTraitRule.php new file mode 100644 index 0000000000..8593429e98 --- /dev/null +++ b/src/Testing/NonexistentAnalysedTraitRule.php @@ -0,0 +1,49 @@ + + */ +final class NonexistentAnalysedTraitRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->namespacedName === null) { + throw new ShouldNotHappenException(); + } + $traitName = $node->namespacedName->toString(); + if ($this->reflectionProvider->hasClass($traitName)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Trait %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $traitName, + )) + ->identifier('phpstan.traitNotFound') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 6bda3dd2bf..d294f81d24 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -2,35 +2,53 @@ namespace PHPStan\Testing; -use PHPStan\Analyser\DirectScopeFactory; -use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\ConstantResolver; +use PHPStan\Analyser\DirectInternalScopeFactory; +use PHPStan\Analyser\Error; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\ScopeFactory; use PHPStan\Analyser\TypeSpecifier; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; -use PHPStan\Broker\Broker; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileHelper; -use PHPStan\Parser\CachedParser; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Parser\Parser; +use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\TypeAliasResolver; +use PHPStan\Type\UsefulTypeAliasResolver; +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; +use function array_merge; +use function count; +use function implode; +use function rtrim; +use function sha1; +use function sprintf; +use function sys_get_temp_dir; +use const DIRECTORY_SEPARATOR; +use const PHP_VERSION_ID; /** @api */ -abstract class PHPStanTestCase extends \PHPUnit\Framework\TestCase +abstract class PHPStanTestCase extends TestCase { - /** @var bool */ - public static $useStaticReflectionProvider = false; - /** @var array */ private static array $containers = []; @@ -39,15 +57,14 @@ public static function getContainer(): Container { $additionalConfigFiles = static::getAdditionalConfigFiles(); $additionalConfigFiles[] = __DIR__ . '/TestCase.neon'; - if (self::$useStaticReflectionProvider) { - $additionalConfigFiles[] = __DIR__ . '/TestCase-staticReflection.neon'; - } $cacheKey = sha1(implode("\n", $additionalConfigFiles)); if (!isset(self::$containers[$cacheKey])) { $tmpDir = sys_get_temp_dir() . '/phpstan-tests'; - if (!@mkdir($tmpDir, 0777) && !is_dir($tmpDir)) { - self::fail(sprintf('Cannot create temp directory %s', $tmpDir)); + try { + DirectoryCreator::ensureDirectoryExists($tmpDir, 0777); + } catch (DirectoryCreatorException $e) { + self::fail($e->getMessage()); } $rootDir = __DIR__ . '/../..'; @@ -64,6 +81,16 @@ public static function getContainer(): Container 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]; @@ -77,76 +104,85 @@ public static function getAdditionalConfigFiles(): array return []; } - public function getParser(): \PHPStan\Parser\Parser + public static function getParser(): Parser { - /** @var \PHPStan\Parser\Parser $parser */ - $parser = self::getContainer()->getByType(CachedParser::class); + /** @var Parser $parser */ + $parser = self::getContainer()->getService('defaultAnalysisParser'); return $parser; } - /** - * @api - * @deprecated Use createReflectionProvider() instead - */ - public function createBroker(): Broker - { - return self::getContainer()->getByType(Broker::class); - } - /** @api */ - public function createReflectionProvider(): ReflectionProvider + public static function createReflectionProvider(): ReflectionProvider { return self::getContainer()->getByType(ReflectionProvider::class); } - /** - * @return array{ClassReflector, FunctionReflector, ConstantReflector} - */ - public static function getReflectors(): array + public static function getReflector(): Reflector { - return [ - self::getContainer()->getService('betterReflectionClassReflector'), - self::getContainer()->getService('betterReflectionFunctionReflector'), - self::getContainer()->getService('betterReflectionConstantReflector'), - ]; + return self::getContainer()->getService('betterReflectionReflector'); } - public function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider + public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider { return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); } - public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier): ScopeFactory + /** + * @param string[] $dynamicConstantNames + */ + public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory { $container = self::getContainer(); - return new DirectScopeFactory( - MutatingScope::class, - $reflectionProvider, - $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), + if (count($dynamicConstantNames) === 0) { + $dynamicConstantNames = $container->getParameter('dynamicConstantNames'); + } + + $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); + $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class); + $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory, $container); + + $initializerExprTypeResolver = new InitializerExprTypeResolver( + $constantResolver, + $reflectionProviderProvider, + $container->getByType(PhpVersion::class), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), - new \PhpParser\PrettyPrinter\Standard(), - $typeSpecifier, - new PropertyReflectionFinder(), - $this->getParser(), - self::getContainer()->getByType(NodeScopeResolver::class), - $this->shouldTreatPhpDocTypesAsCertain(), - $container + 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, new PropertyReflectionFinder()), + $container->getByType(PhpVersion::class), + $container->getByType(AttributeReflectionFactory::class), + $container->getParameter('phpVersion'), + $constantResolver, + ), ); } /** * @param array $globalTypeAliases */ - public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver { $container = self::getContainer(); - return new TypeAliasResolver( + return new UsefulTypeAliasResolver( $globalTypeAliases, $container->getByType(TypeStringResolver::class), $container->getByType(TypeNodeResolver::class), - $reflectionProvider + $reflectionProvider, ); } @@ -155,7 +191,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return true; } - public function getFileHelper(): FileHelper + public static function getFileHelper(): FileHelper { return self::getContainer()->getByType(FileHelper::class); } @@ -163,9 +199,6 @@ public function getFileHelper(): FileHelper /** * 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 { @@ -175,6 +208,27 @@ protected function assertSamePaths(string $expected, string $actual, string $mes $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 === '\\') { diff --git a/src/Testing/PHPUnit/ContainerInitializer.php b/src/Testing/PHPUnit/ContainerInitializer.php new file mode 100644 index 0000000000..8f29c59aa9 --- /dev/null +++ b/src/Testing/PHPUnit/ContainerInitializer.php @@ -0,0 +1,20 @@ +testMethod()->className(); + + if (!is_a($testClassName, PhpStanTestCase::class, true)) { + return; + } + + ContainerInitializer::initialize($testClassName); + } + +} diff --git a/src/Testing/PHPUnit/InitContainerBeforeTestSubscriber.php b/src/Testing/PHPUnit/InitContainerBeforeTestSubscriber.php new file mode 100644 index 0000000000..b735e78c87 --- /dev/null +++ b/src/Testing/PHPUnit/InitContainerBeforeTestSubscriber.php @@ -0,0 +1,32 @@ +test(); + if (!$test->isTestMethod()) { + // skip PHPT tests + return; + } + + $testClassName = $test->className(); + + if (!is_a($testClassName, PhpStanTestCase::class, true)) { + return; + } + + ContainerInitializer::initialize($testClassName); + } + +} diff --git a/src/Testing/PHPUnit/PHPStanPHPUnitExtension.php b/src/Testing/PHPUnit/PHPStanPHPUnitExtension.php new file mode 100644 index 0000000000..7a4ec99267 --- /dev/null +++ b/src/Testing/PHPUnit/PHPStanPHPUnitExtension.php @@ -0,0 +1,29 @@ +registerSubscriber( + new InitContainerBeforeDataProviderSubscriber(), + ); + $facade->registerSubscriber( + new InitContainerBeforeTestSubscriber(), + ); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index e3c4a67e4c..47c8a997e9 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -2,80 +2,138 @@ namespace PHPStan\Testing; +use PhpParser\Node; use PHPStan\Analyser\Analyser; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\IgnoreErrorExtensionProvider; +use PHPStan\Analyser\InternalError; +use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RuleErrorTransformer; use PHPStan\Analyser\TypeSpecifier; +use PHPStan\Collectors\Collector; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; +use PHPStan\File\FileReader; +use PHPStan\Fixable\Patcher; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Rules\Registry; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Properties\DirectReadWritePropertiesExtensionProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtension; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function array_merge; +use function count; +use function implode; +use function sprintf; +use function str_replace; /** * @api - * @template TRule of \PHPStan\Rules\Rule + * @template TRule of Rule */ -abstract class RuleTestCase extends \PHPStan\Testing\PHPStanTestCase +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 self::getContainer()->getService('typeSpecifier'); } - private function getAnalyser(): Analyser + private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser { if ($this->analyser === null) { - $registry = new Registry([ - $this->getRule(), - ]); + $collectorRegistry = new CollectorRegistry($this->getCollectors()); $reflectionProvider = $this->createReflectionProvider(); $typeSpecifier = $this->getTypeSpecifier(); + + $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); $nodeScopeResolver = new NodeScopeResolver( $reflectionProvider, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), + self::getContainer()->getByType(DeprecationProvider::class), + self::getContainer()->getByType(AttributeReflectionFactory::class), self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), + $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), + self::getContainer()->getParameter('polluteScopeWithBlock'), [], [], - true + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + $this->shouldTreatPhpDocTypesAsCertain(), + $this->shouldNarrowMethodScopeFromConstructor(), ); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), - true + new IgnoreErrorExtensionProvider(self::getContainer()), + self::getContainer()->getByType(RuleErrorTransformer::class), + new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( $fileAnalyser, - $registry, + $ruleRegistry, + $collectorRegistry, $nodeScopeResolver, - 50 + 50, ); } @@ -84,22 +142,11 @@ private function getAnalyser(): Analyser /** * @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, - null, - null, - true - ); - if (count($analyserResult->getInternalErrors()) > 0) { - $this->fail(implode("\n", $analyserResult->getInternalErrors())); - } - $actualErrors = $analyserResult->getUnorderedErrors(); - + [$actualErrors, $delayedErrors] = $this->gatherAnalyserErrorsWithDelayedErrors($files); $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string { $message = sprintf('%02d: %s', $line, $message); if ($tip !== null) { @@ -110,16 +157,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( @@ -130,15 +169,118 @@ static function (Error $error) use ($strictlyTypedSprintf): string { } return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip()); }, - $actualErrors + $actualErrors, + ); + + $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); + } + + public function fix(string $file, string $expectedFile): void + { + [$errors] = $this->gatherAnalyserErrorsWithDelayedErrors([$file]); + $diffs = []; + foreach ($errors as $error) { + if ($error->getFixedErrorDiff() === null) { + continue; + } + $diffs[] = $error->getFixedErrorDiff(); + } + + $patcher = self::getContainer()->getByType(Patcher::class); + $newFileContents = $patcher->applyDiffs($file, $diffs); // @phpstan-ignore missingType.checkedException, missingType.checkedException + + $fixedFileContents = FileReader::read($expectedFile); + + $this->assertSame($this->normalizeLineEndings($fixedFileContents), $this->normalizeLineEndings($newFileContents)); + } + + private function normalizeLineEndings(string $string): string + { + return str_replace("\r\n", "\n", $string); + } + + /** + * @param string[] $files + * @return list + */ + public function gatherAnalyserErrors(array $files): array + { + return $this->gatherAnalyserErrorsWithDelayedErrors($files)[0]; + } + + /** + * @param string[] $files + * @return array{list, list} + */ + private function gatherAnalyserErrorsWithDelayedErrors(array $files): array + { + $reflectionProvider = $this->createReflectionProvider(); + $classRule = new DelayedRule(new NonexistentAnalysedClassRule($reflectionProvider)); + $traitRule = new DelayedRule(new NonexistentAnalysedTraitRule($reflectionProvider)); + $ruleRegistry = new DirectRuleRegistry([ + $this->getRule(), + $classRule, + $traitRule, + ]); + $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); + $analyserResult = $this->getAnalyser($ruleRegistry)->analyse( + $files, + null, + null, + true, + ); + if (count($analyserResult->getInternalErrors()) > 0) { + $this->fail(implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()))); + } + + if ($this->shouldFailOnPhpErrors() && count($analyserResult->getAllPhpErrors()) > 0) { + $this->fail(implode("\n", array_map( + static fn (Error $error): string => sprintf('%s on %s:%d', $error->getMessage(), $error->getFile(), $error->getLine()), + $analyserResult->getAllPhpErrors(), + ))); + } + + $finalizer = new AnalyserResultFinalizer( + $ruleRegistry, + new IgnoreErrorExtensionProvider(self::getContainer()), + self::getContainer()->getByType(RuleErrorTransformer::class), + $this->createScopeFactory($reflectionProvider, $this->getTypeSpecifier()), + new LocalIgnoresProcessor(), + true, ); - $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n"); + return [ + $finalizer->finalize($analyserResult, false, true)->getAnalyserResult()->getUnorderedErrors(), + array_merge($classRule->getDelayedErrors(), $traitRule->getDelayedErrors()), + ]; } protected function shouldPolluteScopeWithLoopInitialAssignments(): bool { - return false; + return true; } protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool @@ -146,6 +288,16 @@ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool return true; } + protected function shouldFailOnPhpErrors(): bool + { + return true; + } + + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return false; + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/src/Testing/TestCase-staticReflection.neon b/src/Testing/TestCase-staticReflection.neon deleted file mode 100644 index b79fe6b608..0000000000 --- a/src/Testing/TestCase-staticReflection.neon +++ /dev/null @@ -1,51 +0,0 @@ -services: - - - class: PHPStan\Testing\TestCaseSourceLocatorFactory - arguments: - phpParser: @phpParserDecorator - php8Parser: @php8PhpParser - - currentPhpVersionLexer: - class: PhpParser\Lexer - factory: PhpParser\Lexer\Emulative - arguments: - options: - usedAttributes: [comments, startLine, endLine, startTokenPos, endTokenPos] - - 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: PHPStan\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 index 923de028e2..2682e8dac3 100644 --- a/src/Testing/TestCase.neon +++ b/src/Testing/TestCase.neon @@ -1,6 +1,38 @@ parameters: inferPrivatePropertyTypeFromConstructor: true + services: + - + class: PHPStan\Testing\TestCaseSourceLocatorFactory + arguments: + phpParser: @phpParserDecorator + php8Parser: @php8PhpParser + fileExtensions: %fileExtensions% + excludePaths: %excludePaths% + + # overrides service from services.neon cacheStorage: class: PHPStan\Cache\MemoryCacheStorage arguments!: [] + + # overrides service from parsers.neon + currentPhpVersionSimpleParser!: + factory: @currentPhpVersionRichParser + + # overrides service from parsers.neon + currentPhpVersionLexer: + class: PhpParser\Lexer + factory: @PHPStan\Parser\LexerFactory::createEmulative() + + # overrides service from services.neon + betterReflectionSourceLocator: + class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator + factory: @PHPStan\Testing\TestCaseSourceLocatorFactory::create() + autowired: false + + # overrides service from services.neon + reflectionProvider: + factory: @betterReflectionProvider + arguments!: [] + autowired: + - PHPStan\Reflection\ReflectionProvider diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index 99a6dc38d3..9cddb78e99 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -3,7 +3,7 @@ namespace PHPStan\Testing; use Composer\Autoload\ClassLoader; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; @@ -12,76 +12,87 @@ use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\DependencyInjection\Container; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; +use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; - -class TestCaseSourceLocatorFactory +use ReflectionClass; +use function dirname; +use function is_file; +use function serialize; +use function sha1; +use const PHP_VERSION_ID; + +final class TestCaseSourceLocatorFactory { - private Container $container; - - private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker; - - private AutoloadSourceLocator $autoloadSourceLocator; - - private \PhpParser\Parser $phpParser; - - private \PhpParser\Parser $php8Parser; - - private PhpStormStubsSourceStubber $phpstormStubsSourceStubber; - - private ReflectionSourceStubber $reflectionSourceStubber; + /** @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, - \PhpParser\Parser $php8Parser, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber, - ReflectionSourceStubber $reflectionSourceStubber + private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + 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->php8Parser = $php8Parser; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; } public function create(): SourceLocator { - $classLoaderReflection = new \ReflectionClass(ClassLoader::class); - if ($classLoaderReflection->getFileName() === false) { - throw new \PHPStan\ShouldNotHappenException('Unknown ClassLoader filename'); - } - - $composerProjectPath = dirname($classLoaderReflection->getFileName(), 3); - if (!is_file($composerProjectPath . '/composer.json')) { - throw new \PHPStan\ShouldNotHappenException(sprintf('composer.json not found in directory %s', $composerProjectPath)); - } - - $composerSourceLocator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); - if ($composerSourceLocator === null) { - throw new \PHPStan\ShouldNotHappenException('Could not create composer source locator'); + $classLoaders = ClassLoader::getRegisteredLoaders(); + $classLoaderReflection = new ReflectionClass(ClassLoader::class); + $cacheKey = sha1(serialize([ + $this->phpVersion->getVersionId(), + $this->fileExtensions, + $this->excludePaths, + ])); + if ($classLoaderReflection->hasProperty('vendorDir') && ! isset(self::$composerSourceLocatorsCache[$cacheKey])) { + $composerLocators = [ + $this->optimizedSingleFileSourceLocatorRepository->getOrCreate( + PHP_VERSION_ID < 80500 + ? __DIR__ . '/../../stubs/runtime/Attribute84.php' + : __DIR__ . '/../../stubs/runtime/Attribute85.php', + ), + ]; + $vendorDirProperty = $classLoaderReflection->getProperty('vendorDir'); + if (PHP_VERSION_ID < 80100) { + $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'); - }); - $astPhp8Locator = new Locator($this->php8Parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); + $locators = self::$composerSourceLocatorsCache[$cacheKey] ?? []; + $astLocator = new Locator($this->phpParser); + $astPhp8Locator = new Locator($this->php8Parser); $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber); - $locators[] = $this->autoloadSourceLocator; + $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); $locators[] = new PhpVersionBlacklistSourceLocator(new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 31f1367eb9..be8065f52a 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -5,116 +5,231 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; -use PHPStan\Analyser\DirectScopeFactory; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; +use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; +use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use Symfony\Component\Finder\Finder; +use function array_map; +use function array_merge; +use function count; +use function fclose; +use function fgets; +use function fopen; +use function in_array; +use function is_dir; +use function is_string; +use function preg_match; +use function sprintf; +use function str_starts_with; +use function stripos; +use function strtolower; +use function version_compare; +use const PHP_VERSION; /** @api */ -abstract class TypeInferenceTestCase extends \PHPStan\Testing\PHPStanTestCase +abstract class TypeInferenceTestCase extends PHPStanTestCase { /** - * @param string $file - * @param callable(\PhpParser\Node, \PHPStan\Analyser\Scope): void $callback + * @param callable(Node , Scope ): void $callback * @param string[] $dynamicConstantNames */ - public function processFile( + public static function processFile( string $file, callable $callback, - array $dynamicConstantNames = [] + array $dynamicConstantNames = [], ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); $fileHelper = self::getContainer()->getByType(FileHelper::class); $resolver = new NodeScopeResolver( $reflectionProvider, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), - $this->getParser(), + 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(ParameterClosureThisExtensionProvider::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, - true, - $this->getEarlyTerminatingMethodCalls(), - $this->getEarlyTerminatingFunctionCalls(), - true ); - $resolver->setAnalysedFiles(array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath($file); - }, array_merge([$file], $this->getAdditionalAnalysedFiles()))); - - $scopeFactory = $this->createScopeFactory($reflectionProvider, $typeSpecifier); - if (count($dynamicConstantNames) > 0) { - $reflectionProperty = new \ReflectionProperty(DirectScopeFactory::class, 'dynamicConstantNames'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($scopeFactory, $dynamicConstantNames); - } + $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( - $this->getParser()->parseFile($file), + self::getParser()->parseFile($file), $scope, - $callback + $callback, ); } /** * @api - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function assertFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { if ($assertType === 'type') { - $expectedType = $args[0]; - $expected = $expectedType->getValue(); - $actualType = $args[1]; - $actual = $actualType->describe(VerbosityLevel::precise()); + if ($args[0] instanceof Type) { + // backward compatibility + $expectedType = $args[0]; + $this->assertInstanceOf(ConstantScalarType::class, $expectedType); + $expected = $expectedType->getValue(); + $actualType = $args[1]; + $actual = $actualType->describe(VerbosityLevel::precise()); + } else { + $expected = $args[0]; + $actual = $args[1]; + } + + $failureMessage = sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]); + + $delayedErrors = $args[3] ?? []; + if (count($delayedErrors) > 0) { + $failureMessage .= sprintf( + "\n\nThis failure might be reported because of the following misconfiguration %s:\n\n", + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + foreach ($delayedErrors as $delayedError) { + $failureMessage .= sprintf("* %s\n", $delayedError); + } + } + $this->assertSame( $expected, $actual, - sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]) + $failureMessage, + ); + } elseif ($assertType === 'superType') { + $expected = $args[0]; + $actual = $args[1]; + $isCorrect = $args[2]; + + $failureMessage = sprintf('Expected subtype of %s, got type %s in %s on line %d.', $expected, $actual, $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( + $isCorrect, + $failureMessage, ); } elseif ($assertType === 'variableCertainty') { $expectedCertainty = $args[0]; $actualCertainty = $args[1]; $variableName = $args[2]; + + $failureMessage = sprintf('Expected %s, actual certainty of %s is %s in %s on line %d.', $expectedCertainty->describe(), $variableName, $actualCertainty->describe(), $file, $args[3]); + $delayedErrors = $args[4] ?? []; + if (count($delayedErrors) > 0) { + $failureMessage .= sprintf( + "\n\nThis failure might be reported because of the following misconfiguration %s:\n\n", + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + foreach ($delayedErrors as $delayedError) { + $failureMessage .= sprintf("* %s\n", $delayedError); + } + } + $this->assertTrue( $expectedCertainty->equals($actualCertainty), - sprintf('Expected %s, actual certainty of variable $%s is %s', $expectedCertainty->describe(), $variableName, $actualCertainty->describe()) + $failureMessage, ); } } /** * @api - * @param string $file * @return array */ - public function gatherAssertTypes(string $file): array + public static function gatherAssertTypes(string $file): array { + $fileHelper = self::getContainer()->getByType(FileHelper::class); + + $relativePathHelper = new SystemAgnosticSimpleRelativePathHelper($fileHelper); + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + + $file = $fileHelper->normalizePath($file); + $asserts = []; - $this->processFile($file, function (Node $node, Scope $scope) use (&$asserts, $file): void { + $delayedErrors = []; + self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, &$delayedErrors, $file, $relativePathHelper, $reflectionProvider, $typeStringResolver): void { + if ($node instanceof InClassNode) { + if (!$reflectionProvider->hasClass($node->getClassReflection()->getName())) { + $delayedErrors[] = sprintf( + '%s %s in %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $node->getClassReflection()->getClassTypeDescription(), + $node->getClassReflection()->getName(), + $file, + ); + } + } elseif ($node instanceof Node\Stmt\Trait_) { + if ($node->namespacedName === null) { + throw new ShouldNotHappenException(); + } + if (!$reflectionProvider->hasClass($node->namespacedName->toString())) { + $delayedErrors[] = sprintf('Trait %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', $node->namespacedName->toString()); + } + } if (!$node instanceof Node\Expr\FuncCall) { return; } @@ -125,76 +240,229 @@ public function gatherAssertTypes(string $file): array } $functionName = $nameNode->toString(); - if ($functionName === 'PHPStan\\Testing\\assertType') { + if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertsupertype', 'assertvariablecertainty'], true)) { + self::fail(sprintf( + 'Missing use statement for %s() in %s on line %d.', + $functionName, + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } elseif ($functionName === 'PHPStan\\Testing\\assertType') { $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } $actualType = $scope->getType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { - $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedType = $nativeScope->getNativeType($node->getArgs()[0]->value); - $actualType = $nativeScope->getNativeType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } + + $actualType = $scope->getNativeType($node->getArgs()[1]->value); + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; + } elseif ($functionName === 'PHPStan\\Testing\\assertSuperType') { + $expectedType = $scope->getType($node->getArgs()[0]->value); + $expectedTypeStrings = $expectedType->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { + self::fail(sprintf( + 'Expected super 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); + $isCorrect = $typeStringResolver->resolve($expectedTypeStrings[0]->getValue())->isSuperTypeOf($actualType)->yes(); + + $assert = ['superType', $file, $expectedTypeStrings[0]->getValue(), $actualType->describe(VerbosityLevel::precise()), $isCorrect, $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { - $this->fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); + self::fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); } if (!$certainty->class instanceof Node\Name) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if (!$certainty->name instanceof Node\Identifier) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $node->getArgs()[1]->value; - if (!$variable instanceof Node\Expr\Variable) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); - } - if (!is_string($variable->name)) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); + if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) { + $actualCertaintyValue = $scope->hasVariableType($variable->name); + $variableDescription = sprintf('variable $%s', $variable->name); + } elseif ($variable instanceof Node\Expr\ArrayDimFetch && $variable->dim !== null) { + $offset = $scope->getType($variable->dim); + $actualCertaintyValue = $scope->getType($variable->var)->hasOffsetValueType($offset); + $variableDescription = sprintf('offset %s', $offset->describe(VerbosityLevel::precise())); + } else { + self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } - $actualCertaintyValue = $scope->hasVariableType($variable->name); - $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name]; + $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variableDescription, $node->getStartLine()]; } else { - return; + $correctFunction = null; + + $assertFunctions = [ + 'assertType' => 'PHPStan\\Testing\\assertType', + 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType', + 'assertSuperType' => 'PHPStan\\Testing\\assertSuperType', + 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty', + ]; + foreach ($assertFunctions as $assertFn => $fqFunctionName) { + if (stripos($functionName, $assertFn) === false) { + continue; + } + + $correctFunction = $fqFunctionName; + } + + if ($correctFunction === null) { + return; + } + + self::fail(sprintf( + 'Function %s imported with wrong namespace %s called in %s on line %d.', + $correctFunction, + $functionName, + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); } if (count($node->getArgs()) !== 2) { - $this->fail(sprintf( - 'ERROR: Wrong %s() call on line %d.', + self::fail(sprintf( + 'ERROR: Wrong %s() call in %s on line %d.', $functionName, - $node->getLine() + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), )); } - $asserts[$file . ':' . $node->getLine()] = $assert; + $asserts[$file . ':' . $node->getStartLine()] = $assert; }); + if (count($asserts) === 0) { + self::fail(sprintf('File %s does not contain any asserts', $file)); + } + + if (count($delayedErrors) === 0) { + return $asserts; + } + + foreach ($asserts as $i => $assert) { + $assert[] = $delayedErrors; + $asserts[$i] = $assert; + } + return $asserts; } + /** + * @api + * @return array + */ + public static function gatherAssertTypesFromDirectory(string $directory): array + { + $asserts = []; + foreach (self::findTestDataFilesFromDirectory($directory) as $path) { + foreach (self::gatherAssertTypes($path) as $key => $assert) { + $asserts[$key] = $assert; + } + } + + return $asserts; + } + + /** + * @return list + */ + public static function findTestDataFilesFromDirectory(string $directory): array + { + if (!is_dir($directory)) { + self::fail(sprintf('Directory %s does not exist.', $directory)); + } + + $finder = new Finder(); + $finder->followLinks(); + $files = []; + foreach ($finder->files()->name('*.php')->in($directory) as $fileInfo) { + $path = $fileInfo->getPathname(); + if (self::isFileLintSkipped($path)) { + continue; + } + $files[] = $path; + } + + return $files; + } + + /** + * From https://github.com/php-parallel-lint/PHP-Parallel-Lint/blob/0c2706086ac36dce31967cb36062ff8915fe03f7/bin/skip-linting.php + * + * Copyright (c) 2012, Jakub Onderka + */ + private static function isFileLintSkipped(string $file): bool + { + $f = @fopen($file, 'r'); + if ($f !== false) { + $firstLine = fgets($f); + if ($firstLine === false) { + return false; + } + + // ignore shebang line + if (str_starts_with($firstLine, '#!')) { + $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]; } @@ -78,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 @@ -90,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 @@ -136,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..6ed6baeced --- /dev/null +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -0,0 +1,508 @@ +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 + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return $this; + } + + return new ErrorType(); + } + + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + 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, ?TrinaryLogic $strict = null): 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this; + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getIterableKeyType(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getFirstIterableKeyType(): Type + { + return new ConstantIntegerType(0); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getIterableValueType(): Type + { + return new MixedType(); + } + + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + } + + public function toFloat(): Type + { + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return $this; + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('list'); + } + +} diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 2cbf3134f7..d8a02005d6 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -2,33 +2,47 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class AccessoryLiteralStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() @@ -40,44 +54,59 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof MixedType) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isLiteralString(); + return new AcceptsResult($type->isLiteralString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isLiteralString(); + return new IsSuperTypeOfResult($type->isLiteralString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isLiteralString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isLiteralString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -85,7 +114,7 @@ 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 'literal-string'; } @@ -95,9 +124,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -110,21 +144,38 @@ public function getOffsetValueType(Type $offsetType): Type } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isLiteralString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { return $this; } - public function isArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type { - return new UnionType([ - $this->toInteger(), - $this->toFloat(), - ]); + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); } public function toInteger(): Type @@ -152,10 +203,80 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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(); @@ -166,19 +287,92 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type + 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 self(); + return new IdentifierTypeNode('literal-string'); } } diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php new file mode 100644 index 0000000000..3e1383bdb4 --- /dev/null +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -0,0 +1,383 @@ +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], + isList: TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ( + $type->isString()->yes() + && $type->isLowercaseString()->no() + && ($type->isNumericString()->no() || $this->isNumericString()->no()) + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('lowercase-string'); + } + +} diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 2e3f599e7e..a8b573809a 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -2,33 +2,48 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; -use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class AccessoryNonEmptyStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; - use TruthyBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use UndecidedBooleanTypeTrait; /** @api */ public function __construct() @@ -40,41 +55,63 @@ 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->isNonEmptyString()->yes()) { + return AcceptsResult::createYes(); + } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isNonEmptyString(); + return new AcceptsResult($type->isNonEmptyString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isNonEmptyString(); + if ($type->isNonFalsyString()->yes()) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isNonEmptyString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isNonEmptyString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNonEmptyString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -82,7 +119,7 @@ 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 'non-empty-string'; } @@ -92,9 +129,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -112,20 +154,33 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + return $this; } - public function isArray(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createNo(); + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); } public function toNumber(): Type { - return new UnionType([ - $this->toInteger(), - $this->toFloat(), - ]); + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); } public function toInteger(): Type @@ -148,10 +203,80 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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(); @@ -162,19 +287,104 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function 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 static function __set_state(array $properties): Type + 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 self(); + return new IdentifierTypeNode('non-empty-string'); } } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php new file mode 100644 index 0000000000..f32d613154 --- /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 + { + // Do not remove `0` since `(int) '00'` is still `0`. + 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], + isList: TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + $falseyTypes = StaticTypeFactory::falsey(); + if ($falseyTypes->isSuperTypeOf($type)->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('non-falsy-string'); + } + +} diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 3463615c91..c10dd28082 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,15 +2,26 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\StringType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; @@ -18,11 +29,14 @@ use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class AccessoryNumericStringType implements CompoundType, AccessoryType { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; @@ -40,41 +54,64 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isNumericString(); + return new AcceptsResult($type->isNumericString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isNumericString(); + return new IsSuperTypeOfResult($type->isNumericString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isNumericString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNumericString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + if ($acceptingType->isNonFalsyString()->yes()) { + return AcceptsResult::createMaybe(); + } + + if ($acceptingType->isNonEmptyString()->yes()) { + return AcceptsResult::createYes(); + } + + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -82,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 'numeric'; + return 'numeric-string'; } public function isOffsetAccessible(): TrinaryLogic @@ -92,9 +129,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -108,12 +150,23 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + return $this; } - public function isArray(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createNo(); + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); } public function toNumber(): Type @@ -124,6 +177,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); @@ -144,10 +202,86 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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(); @@ -158,19 +292,104 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes() && $type->isNumericString()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type + 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 self(); + return new IdentifierTypeNode('numeric-string'); } } diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php new file mode 100644 index 0000000000..5688e62df7 --- /dev/null +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -0,0 +1,383 @@ +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], + isList: 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 9ad20b04f1..b59a77c788 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -2,20 +2,31 @@ 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 { @@ -23,13 +34,12 @@ class HasMethodType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private string $methodName; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(string $methodName) + public function __construct(private string $methodName) { - $this->methodName = $methodName; } public function getReferencedClasses(): array @@ -37,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 @@ -78,7 +106,7 @@ public function equals(Type $type): bool && $this->getCanonicalMethodName() === $type->getCanonicalMethodName(); } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasMethod(%s)', $this->methodName); } @@ -92,7 +120,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -104,9 +132,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -119,6 +145,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 [ @@ -126,14 +161,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 f2f5c41714..da6c176655 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -2,39 +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\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; - - /** @api */ - 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; @@ -45,39 +65,53 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)); + return new AcceptsResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)); + return new IsSuperTypeOfResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isOffsetAccessible() - ->and($otherType->hasOffsetValueType($this->offsetType)) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + $result = new IsSuperTypeOfResult($otherType->isOffsetAccessible()->and($otherType->hasOffsetValueType($this->offsetType)), []); + + return $result + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -86,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)); } @@ -96,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(); } @@ -115,12 +154,140 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return $this; + } + + 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(); } @@ -135,16 +302,81 @@ 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + 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(); @@ -165,14 +397,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..5e7c834942 --- /dev/null +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -0,0 +1,514 @@ +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 + { + if (!$offsetType->equals($this->offsetType)) { + return $this; + } + + 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + 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, ?TrinaryLogic $strict = null): Type + { + $strict ??= TrinaryLogic::createMaybe(); + if ( + $needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType + && ( + $needleType->getValue() === $this->valueType->getValue() + // @phpstan-ignore equal.notAllowed + || ($strict->no() && $needleType->getValue() == $this->valueType->getValue()) // phpcs:ignore + ) + ) { + return new UnionType([ + new IntegerType(), + new StringType(), + ]); + } + + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return $this; + } + + 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 b669c3a085..ba2dbf8892 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,16 +2,25 @@ 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 { @@ -19,56 +28,77 @@ class HasPropertyType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private string $propertyName; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(string $propertyName) + public function __construct(private string $propertyName) { - $this->propertyName = $propertyName; } - /** - * @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->hasInstanceProperty($this->propertyName)->or($type->hasStaticProperty($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->hasInstanceProperty($this->propertyName)->or($otherType->hasStaticProperty($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 @@ -77,7 +107,7 @@ public function equals(Type $type): bool && $this->propertyName === $type->propertyName; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasProperty(%s)', $this->propertyName); } @@ -91,19 +121,57 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + if ($this->propertyName === $propertyName) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + if ($this->propertyName === $propertyName) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { 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 0e255f70f9..a58f87cb86 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -2,20 +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\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; 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 { @@ -25,6 +36,8 @@ class NonEmptyArrayType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -36,44 +49,69 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + $isArray = $type->isArray(); + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + + return new AcceptsResult($isArray->and($isIterableAtLeastOnce), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + return new IsSuperTypeOfResult($type->isArray()->and($type->isIterableAtLeastOnce()), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isArray() - ->and($otherType->isIterableAtLeastOnce()) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isIterableAtLeastOnce()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -81,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 @@ -91,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(); @@ -106,6 +149,97 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): 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, ?TrinaryLogic $strict = null): 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ( + (new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes() + || $replacementType->toArray()->isIterableAtLeastOnce()->yes() + ) { + return $this; + } + + return new MixedType(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -116,21 +250,116 @@ 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(); @@ -141,16 +370,70 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); @@ -168,7 +451,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 @@ -176,9 +469,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..77fdec48fb --- /dev/null +++ b/src/Type/Accessory/OversizedArrayType.php @@ -0,0 +1,472 @@ +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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + 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, ?TrinaryLogic $strict = null): 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): 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 34080be9d9..9f87759790 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,44 +2,62 @@ 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\Rules\Arrays\AllowedArrayKeysTypes; +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 \PHPStan\Type\Type $itemType; + private Type $keyType; /** @api */ - public function __construct(Type $keyType, Type $itemType) + public function __construct(Type $keyType, private Type $itemType) { if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { $keyType = new MixedType(); } + if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { + $keyType = new UnionType([new StringType(), new IntegerType()]); + } + $this->keyType = $keyType; - $this->itemType = $itemType; } public function getKeyType(): Type @@ -52,30 +70,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 $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; @@ -86,35 +108,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) { @@ -141,28 +162,28 @@ 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, GeneralizePrecision::lessSpecific())); + return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } - public function getKeysArray(): self + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type { - return new self(new IntegerType(), $this->keyType); + return $this->getKeysArray(); } - public function getValuesArray(): self + public function getKeysArray(): Type { - return new self(new IntegerType(), $this->itemType); + return TypeCombinator::intersect(new self(new IntegerType(), $this->getIterableKeyType()), new AccessoryArrayListType()); } - public function isIterable(): TrinaryLogic + public function getValuesArray(): Type { - return TrinaryLogic::createYes(); + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } public function isIterableAtLeastOnce(): TrinaryLogic @@ -170,50 +191,93 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { $keyType = $this->keyType; 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 TrinaryLogic::createYes(); + return $this->getItemType(); } - public function isNumericString(): TrinaryLogic + public function getLastIterableValueType(): Type { - return TrinaryLogic::createNo(); + return $this->getItemType(); } - public function isNonEmptyString(): TrinaryLogic + public function isConstantArray(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isLiteralString(): TrinaryLogic + public function isList(): TrinaryLogic + { + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return TrinaryLogic::createNo(); + } + + if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOffsetAccessible(): TrinaryLogic + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return TrinaryLogic::createYes(); + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = self::castToArrayKeyType($offsetType); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + $offsetArrayKeyType = $offsetType->toArrayKey(); + if ($offsetArrayKeyType instanceof ErrorType) { + $allowedArrayKeys = AllowedArrayKeysTypes::getType(); + $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey(); + } + $offsetType = $offsetArrayKeyType; + + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return TrinaryLogic::createNo(); } @@ -222,8 +286,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(); } @@ -238,129 +304,250 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($offsetType === null) { - $offsetType = new IntegerType(); + $isKeyTypeInteger = $this->keyType->isInteger(); + if ($isKeyTypeInteger->no()) { + $offsetType = new IntegerType(); + } elseif ($isKeyTypeInteger->yes()) { + /** @var list $constantScalars */ + $constantScalars = $this->keyType->getConstantScalarTypes(); + if (count($constantScalars) > 0) { + foreach ($constantScalars as $constantScalar) { + $constantScalars[] = ConstantTypeHelper::getTypeFromValue($constantScalar->getValue() + 1); + } + + $offsetType = TypeCombinator::union(...$constantScalars); + } else { + $offsetType = $this->keyType; + } + } else { + $integerTypes = []; + TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use (&$integerTypes): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $isInteger = $type->isInteger(); + if ($isInteger->yes()) { + $integerTypes[] = $type; + } + + return $type; + }); + if (count($integerTypes) === 0) { + $offsetType = $this->keyType; + } else { + $offsetType = TypeCombinator::union(...$integerTypes); + } + } + } else { + $offsetType = $offsetType->toArrayKey(); + } + + if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { + if ($offsetType->isSuperTypeOf($this->keyType)->yes()) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType($offsetType, $valueType); + return $builder->getArray(); + } + + return TypeCombinator::intersect( + new self( + TypeCombinator::union($this->keyType, $offsetType), + TypeCombinator::union($this->itemType, $valueType), + ), + new HasOffsetValueType($offsetType, $valueType), + new NonEmptyArrayType(), + ); } - return TypeCombinator::intersect(new self( - TypeCombinator::union($this->keyType, self::castToArrayKeyType($offsetType)), - $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType - ), new NonEmptyArrayType()); + return TypeCombinator::intersect( + new self( + TypeCombinator::union($this->keyType, $offsetType), + $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType, + ), + new NonEmptyArrayType(), + ); } - public function isCallable(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createMaybe()->and((new StringType())->isSuperTypeOf($this->itemType)); + if ($this->itemType->isConstantArray()->yes() && $valueType->isConstantArray()->yes()) { + $newItemType = $this->itemType; + foreach ($valueType->getConstantArrays() as $constArray) { + foreach ($constArray->getKeyTypes() as $keyType) { + $newItemType = $newItemType->setExistingOffsetValueType($keyType, $constArray->getOffsetValueType($keyType)); + } + } + + if ($newItemType !== $this->itemType) { + return new self( + $this->keyType, + $newItemType, + ); + } + } + + return new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType), + ); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function unsetOffset(Type $offsetType): Type { - if ($this->isCallable()->no()) { - throw new \PHPStan\ShouldNotHappenException(); + $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 [new TrivialParametersAcceptor()]; + return $this; } - public function toNumber(): Type + public function fillKeysArray(Type $valueType): Type { - return new ErrorType(); + $itemType = $this->getItemType(); + if ($itemType->isInteger()->no()) { + $stringKeyType = $itemType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + return new ArrayType($stringKeyType, $valueType); + } + + return new ArrayType($itemType, $valueType); } - public function toString(): Type + public function flipArray(): Type { - return new ErrorType(); + return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType()); } - public function toInteger(): Type + public function intersectKeyArray(Type $otherArraysType): Type { - return TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1) - ); + $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 toFloat(): Type + public function popArray(): Type { - return TypeCombinator::union( - new ConstantFloatType(0.0), - new ConstantFloatType(1.0) - ); + return $this; } - public function toArray(): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type { return $this; } - public function count(): Type + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { - return IntegerRangeType::fromInterval(0, null); + $strict ??= TrinaryLogic::createMaybe(); + if ($strict->yes() && $this->getIterableValueType()->isSuperTypeOf($needleType)->no()) { + return new ConstantBooleanType(false); + } + + return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); } - public static function castToArrayKeyType(Type $offsetType): Type + public function shiftArray(): Type { - return TypeTraverser::map($offsetType, static function (Type $offsetType, callable $traverse): Type { - if ($offsetType instanceof TemplateType) { - return $offsetType; - } + return $this; + } - if ($offsetType instanceof ConstantScalarType) { - /** @var int|string $offsetValue */ - $offsetValue = key([$offsetType->getValue() => null]); - return is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); - } + public function shuffleArray(): Type + { + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); + } - if ($offsetType instanceof IntegerType) { - return $offsetType; - } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return new ConstantArrayType([], []); + } - if ($offsetType instanceof FloatType || $offsetType instanceof BooleanType || $offsetType->isNumericString()->yes()) { - return new IntegerType(); - } + if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) { + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); + } - if ($offsetType instanceof StringType || $offsetType->isNonEmptyString()->yes()) { - return $offsetType; - } + return $this; + } - if ($offsetType instanceof UnionType || $offsetType instanceof IntersectionType) { - return $traverse($offsetType); - } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + $replacementArrayType = $replacementType->toArray(); + $replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce(); - return new UnionType([new IntegerType(), new StringType()]); - }); + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes() && $replacementArrayTypeIsIterableAtLeastOnce->no()) { + return new ConstantArrayType([], []); + } + + $arrayType = new self( + TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()), + TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()), + ); + + if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + return $arrayType; } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function isCallable(): TrinaryLogic { - if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { - return $receivedType->inferTemplateTypesOn($this); + return TrinaryLogic::createMaybe()->and($this->itemType->isString()); + } + + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + if ($this->isCallable()->no()) { + throw new ShouldNotHappenException(); } - 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(), - ]); - } + return [new TrivialParametersAcceptor()]; + } - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $typeMap = $typeMap->union(new TemplateTypeMap([ - $itemType->getName() => $itemType->getBound(), - ])); - } + public function toInteger(): Type + { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + } - return $typeMap; + public function toFloat(): Type + { + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { + return $receivedType->inferTemplateTypesOn($this); } if ($receivedType->isArray()->yes()) { - $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); return $keyTypeMap->union($itemTypeMap); @@ -371,24 +558,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), ); } @@ -398,22 +572,79 @@ 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->isSuperTypeOf(new ConstantArrayType([], []))->yes()) { + 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 92ac19a4f1..6a2d28dc1c 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -4,11 +4,31 @@ 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) . ')'; @@ -33,19 +53,58 @@ protected function unionTypes(callable $getType): Type return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); } + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->getTypes() as $type) { + $innerValues = $getValues($type); + if ($innerValues === [] && $criteria($type)) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function getOffsetValueType(Type $offsetType): Type + { + $types = []; + foreach ($this->getTypes() as $innerType) { + $valueType = $innerType->getOffsetValueType($offsetType); + if ($valueType instanceof ErrorType) { + continue; + } + + $types[] = $valueType; + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + protected function unionResults(callable $getResult): TrinaryLogic { - return TrinaryLogic::createNo()->or(...array_map($getResult, $this->getTypes())); + return TrinaryLogic::createNo()->lazyOr($this->getTypes(), $getResult); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $results = []; + $result = AcceptsResult::createNo(); foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); + $result = $result->or($acceptingType->accepts($innerType, $strictTypes)); } - return TrinaryLogic::createNo()->or(...$results); + return $result; } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -90,13 +149,33 @@ 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 new self($properties['types']); + $types = []; + $changed = false; + + if (!$right instanceof UnionType) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + + return $this; } } diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php new file mode 100644 index 0000000000..37e0a4a5cd --- /dev/null +++ b/src/Type/BitwiseFlagHelper.php @@ -0,0 +1,109 @@ +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 5911facc94..bbde2f2aa8 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,15 +2,22 @@ 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; @@ -19,18 +26,36 @@ 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 { return 'bool'; @@ -41,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'), ); } @@ -53,7 +83,7 @@ public function toInteger(): Type { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } @@ -61,7 +91,7 @@ public function toFloat(): Type { return TypeCombinator::union( new ConstantFloatType(0.0), - new ConstantFloatType(1.0) + new ConstantFloatType(1.0), ); } @@ -70,37 +100,97 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + 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, bool $unionValues = true): 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 397c8c6027..079afc8c1e 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -2,62 +2,108 @@ 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; /** @api */ -class CallableType implements CompoundType, ParametersAcceptor +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; + /** * @api - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @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 = []; @@ -68,24 +114,43 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType && !$type instanceof self) { return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType && !$type instanceof self) { + return $type->isSubTypeOf($this); + } + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { - $isCallable = $type->isCallable(); - if ($isCallable->no() || $this->isCommonCallable) { + $isCallable = new IsSuperTypeOfResult($type->isCallable(), []); + if ($isCallable->no()) { return $isCallable; } @@ -94,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; @@ -105,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 (ParameterReflection $param) use ($level): string { - return sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level)); - }, - $this->getParameters() - )), - $this->returnType->describe($level) + static fn (): string => 'callable', + 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()); + }, ); } @@ -157,20 +252,64 @@ 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 mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -191,18 +330,38 @@ 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 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 { @@ -225,7 +384,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType->isCallable()->no()) { + if (! $receivedType->isCallable()->yes()) { return TemplateTypeMap::createEmpty(); } @@ -242,10 +401,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap { - $typeMap = TemplateTypeMap::createEmpty(); + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false); $args = $parametersAcceptor->getParameters(); $returnType = $parametersAcceptor->getReturnType(); + $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getParameters() as $i => $param) { $paramType = $param->getType(); if (isset($args[$i])) { @@ -265,7 +426,7 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { $references = $this->getReturnType()->getReferencedTemplateTypes( - $positionVariance->compose(TemplateTypeVariance::createCovariant()) + $positionVariance->compose(TemplateTypeVariance::createCovariant()), ); $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant()); @@ -293,18 +454,127 @@ 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(); } @@ -319,26 +589,107 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function isCommonCallable(): bool { return $this->isCommonCallable; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - return new self( - (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], - (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], - $properties['variadic'] + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 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 index 62a946a020..d0502cb9a1 100644 --- a/src/Type/CircularTypeAliasDefinitionException.php +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -2,7 +2,9 @@ namespace PHPStan\Type; -class CircularTypeAliasDefinitionException extends \Exception +use Exception; + +final class CircularTypeAliasDefinitionException extends Exception { } diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 71b3344bd6..c5dae4f195 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -2,9 +2,9 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; /** @api */ class ClassStringType extends StringType @@ -21,48 +21,27 @@ public function describe(VerbosityLevel $level): string return 'class-string'; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - if ($type instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($type->getValue())); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } - - if ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); + return new AcceptsResult($type->isClassString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($type->getValue())); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return new IsSuperTypeOfResult($type->isClassString(), []); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); } public function isNumericString(): TrinaryLogic @@ -75,18 +54,44 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('class-string'); } } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 6fdf33fd2a..49bbfe8c1b 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -2,16 +2,32 @@ 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\ClassReflection; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -21,41 +37,125 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\Traits\NonGenericTypeTrait; +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, ParametersAcceptor +class ClosureType implements TypeWithClassName, CallableParametersAcceptor { - use NonGenericTypeTrait; + use NonArrayTypeTrait; + use NonIterableTypeTrait; use UndecidedComparisonTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - private ObjectType $objectType; - - /** @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; + + private TrinaryLogic $mustUseReturnValue; /** * @api - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @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, + ?TrinaryLogic $mustUseReturnValue = 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; + if ($mustUseReturnValue === null) { + $mustUseReturnValue = TrinaryLogic::createMaybe(); + } + $this->mustUseReturnValue = $mustUseReturnValue; + + $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 @@ -73,9 +173,6 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName return $this->objectType->getAncestorWithClassName($className); } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = $this->objectType->getReferencedClasses(); @@ -86,7 +183,17 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return $this->objectType->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->objectType->getObjectClassReflections(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); @@ -96,29 +203,35 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $this->objectType->accepts($type, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { if ($type instanceof self) { + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$type], false); + if (!$variant instanceof CallableParametersAcceptor) { + return IsSuperTypeOfResult::createNo([]); + } return CallableTypeHelper::isParametersAcceptorSuperTypeOf( $this, - $type, - $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); @@ -130,27 +243,68 @@ public function equals(Type $type): bool return false; } - return $this->returnType->equals($type->returnType); + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()) + && $this->isPure()->equals($type->isPure()); } public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'Closure'; - }, - function () use ($level): string { - return sprintf( - 'Closure(%s): %s', - implode(', ', array_map(static function (ParameterReflection $parameter) use ($level): string { - return sprintf('%s%s', $parameter->isVariadic() ? '...' : '', $parameter->getType()->describe($level)); - }, $this->parameters)), - $this->returnType->describe($level) + static fn (): string => 'Closure', + 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, + $this->acceptsNamedArguments, + $this->mustUseReturnValue, ); - } + + 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(); @@ -161,7 +315,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->objectType->hasProperty($propertyName); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->objectType->getProperty($propertyName, $scope); } @@ -171,6 +325,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $this->objectType->getUnresolvedPropertyPrototype($propertyName, $scope); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->objectType->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->objectType->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->objectType->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->objectType->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->objectType->getStaticProperty($propertyName, $scope); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->objectType->getUnresolvedStaticPropertyPrototype($propertyName, $scope); + } + public function canCallMethods(): TrinaryLogic { return $this->objectType->canCallMethods(); @@ -181,7 +365,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->objectType->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -191,7 +375,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce if ($methodName === 'call') { return new ClosureCallUnresolvedMethodPrototypeReflection( $this->objectType->getUnresolvedMethodPrototype($methodName, $scope), - $this + $this, ); } @@ -208,11 +392,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(); @@ -223,48 +412,54 @@ 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, bool $unionValues = true): 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 mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; } public function isCloneable(): TrinaryLogic @@ -282,6 +477,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -302,22 +502,38 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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 { @@ -340,7 +556,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(); } @@ -357,10 +573,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap { - $typeMap = TemplateTypeMap::createEmpty(); + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false); $args = $parametersAcceptor->getParameters(); $returnType = $parametersAcceptor->getReturnType(); + $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getParameters() as $i => $param) { $paramType = $param->getType(); if (isset($args[$i])) { @@ -377,8 +595,29 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType)); } + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + $references = $this->getReturnType()->getReferencedTemplateTypes( + $positionVariance->compose(TemplateTypeVariance::createCovariant()), + ); + + $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant()); + + foreach ($this->getParameters() as $param) { + foreach ($param->getType()->getReferencedTemplateTypes($paramVariance) as $reference) { + $references[] = $reference; + } + } + + return $references; + } + public function traverse(callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + return new self( array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); @@ -388,15 +627,126 @@ 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, + $this->mustUseReturnValue, + ); + } + + 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, + $this->mustUseReturnValue, ); } - 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(); } @@ -411,21 +761,97 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic { - return new self( - $properties['parameters'], - $properties['returnType'], - $properties['variadic'] + 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..ef4d81be75 --- /dev/null +++ b/src/Type/ClosureTypeFactory.php @@ -0,0 +1,142 @@ +reflectionSourceStubber->generateFunctionStubFromReflection($closureReflectionFunction); + 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()), isVariadic: $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()); + + $selfClass = null; + if (method_exists($closureReflectionFunction, 'getClosureCalledClass') && $closureReflectionFunction->getClosureCalledClass() !== null) { + $potentialSelfClassName = $closureReflectionFunction->getClosureCalledClass()->getName(); + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if ($reflectionProvider->hasClass($potentialSelfClassName)) { + $selfClass = $reflectionProvider->getClass($potentialSelfClassName); + } + } elseif ($closureReflectionFunction->getClosureScopeClass() !== null) { + $potentialSelfClassName = $closureReflectionFunction->getClosureScopeClass()->getName(); + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if ($reflectionProvider->hasClass($potentialSelfClassName)) { + $selfClass = $reflectionProvider->getClass($potentialSelfClassName); + } + } + + return new ClosureType($parameters, TypehintHelper::decideTypeFromReflection(ReflectionType::fromTypeOrNull($betterReflectionFunction->getReturnType()), selfClass: $selfClass), $betterReflectionFunction->isVariadic()); + } + +} diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index 199c516c4b..f66e10c091 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -2,18 +2,19 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; /** @api */ interface CompoundType extends Type { - public function isSubTypeOf(Type $otherType): TrinaryLogic; + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult; - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic; + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult; - public function isGreaterThan(Type $otherType): TrinaryLogic; + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic; + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; } diff --git a/src/Type/ConditionalType.php b/src/Type/ConditionalType.php new file mode 100644 index 0000000000..f154fb5368 --- /dev/null +++ b/src/Type/ConditionalType.php @@ -0,0 +1,220 @@ +subject; + } + + public function getTarget(): Type + { + return $this->target; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->if->isSuperTypeOf($type->if) + ->and($this->else->isSuperTypeOf($type->else)); + } + + return $this->isSuperTypeOfDefault($type); + } + + public function getReferencedClasses(): array + { + return array_merge( + $this->subject->getReferencedClasses(), + $this->target->getReferencedClasses(), + $this->if->getReferencedClasses(), + $this->else->getReferencedClasses(), + ); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return array_merge( + $this->subject->getReferencedTemplateTypes($positionVariance), + $this->target->getReferencedTemplateTypes($positionVariance), + $this->if->getReferencedTemplateTypes($positionVariance), + $this->else->getReferencedTemplateTypes($positionVariance), + ); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->subject->equals($type->subject) + && $this->target->equals($type->target) + && $this->if->equals($type->if) + && $this->else->equals($type->else); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + $this->subject->describe($level), + $this->negated ? 'is not' : 'is', + $this->target->describe($level), + $this->if->describe($level), + $this->else->describe($level), + ); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target); + } + + protected function getResult(): Type + { + $isSuperType = $this->target->isSuperTypeOf($this->subject); + + if ($isSuperType->yes()) { + return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); + } + + if ($isSuperType->no()) { + return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); + } + + return TypeCombinator::union( + $this->getNormalizedIf(), + $this->getNormalizedElse(), + ); + } + + public function traverse(callable $cb): Type + { + $subject = $cb($this->subject); + $target = $cb($this->target); + $if = $cb($this->getNormalizedIf()); + $else = $cb($this->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { + return $this; + } + + return new self($subject, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $subject = $cb($this->subject, $right->subject); + $target = $cb($this->target, $right->target); + $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf()); + $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { + return $this; + } + + return new self($subject, $target, $if, $else, $this->negated); + } + + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeNode( + $this->subject->toPhpDocNode(), + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); + } + + private function getNormalizedIf(): Type + { + return $this->normalizedIf ??= TypeTraverser::map( + $this->if, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType()) + : $traverse($type), + ); + } + + private function getNormalizedElse(): Type + { + return $this->normalizedElse ??= TypeTraverser::map( + $this->else, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType()) + : $traverse($type), + ); + } + + private function getSubjectWithTargetIntersectedType(): Type + { + return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target); + } + + private function getSubjectWithTargetRemovedType(): Type + { + return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target); + } + +} diff --git a/src/Type/ConditionalTypeForParameter.php b/src/Type/ConditionalTypeForParameter.php new file mode 100644 index 0000000000..0fd8cf4475 --- /dev/null +++ b/src/Type/ConditionalTypeForParameter.php @@ -0,0 +1,177 @@ +parameterName; + } + + public function getTarget(): Type + { + return $this->target; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function changeParameterName(string $parameterName): self + { + return new self( + $parameterName, + $this->target, + $this->if, + $this->else, + $this->negated, + ); + } + + public function toConditional(Type $subject): Type + { + return new ConditionalType( + $subject, + $this->target, + $this->if, + $this->else, + $this->negated, + ); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->if->isSuperTypeOf($type->if) + ->and($this->else->isSuperTypeOf($type->else)); + } + + return $this->isSuperTypeOfDefault($type); + } + + public function getReferencedClasses(): array + { + return array_merge( + $this->target->getReferencedClasses(), + $this->if->getReferencedClasses(), + $this->else->getReferencedClasses(), + ); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return array_merge( + $this->target->getReferencedTemplateTypes($positionVariance), + $this->if->getReferencedTemplateTypes($positionVariance), + $this->else->getReferencedTemplateTypes($positionVariance), + ); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->parameterName === $type->parameterName + && $this->target->equals($type->target) + && $this->if->equals($type->if) + && $this->else->equals($type->else); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + $this->parameterName, + $this->negated ? 'is not' : 'is', + $this->target->describe($level), + $this->if->describe($level), + $this->else->describe($level), + ); + } + + public function isResolvable(): bool + { + return false; + } + + protected function getResult(): Type + { + return TypeCombinator::union($this->if, $this->else); + } + + public function traverse(callable $cb): Type + { + $target = $cb($this->target); + $if = $cb($this->if); + $else = $cb($this->else); + + if ($this->target === $target && $this->if === $if && $this->else === $else) { + return $this; + } + + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $target = $cb($this->target, $right->target); + $if = $cb($this->if, $right->if); + $else = $cb($this->else, $right->else); + + if ($this->target === $target && $this->if === $if && $this->else === $else) { + return $this; + } + + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeForParameterNode( + $this->parameterName, + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); + } + +} diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 7a800fc291..b4e2ed6f36 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,92 +2,196 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\Strings; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Internal\CombinationsHelper; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; +use PHPStan\Rules\Arrays\AllowedArrayKeysTypes; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; -use PHPStan\Type\ConstantType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntegerType; 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; +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 extends ArrayType implements ConstantType +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; + } - $this->keyTypes = $keyTypes; - $this->valueTypes = $valueTypes; - $this->nextAutoIndex = $nextAutoIndex; - $this->optionalKeys = $optionalKeys; + public function getIterableValueType(): Type + { + if ($this->iterableValueType !== null) { + return $this->iterableValueType; + } + + return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true); + } + + public function getKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getItemType(): Type + { + return $this->getIterableValueType(); } - public function isEmpty(): bool + public function isConstantValue(): TrinaryLogic { - return count($this->keyTypes) === 0; + return TrinaryLogic::createYes(); } - public function getNextAutoIndex(): int + /** + * @return non-empty-list + */ + public function getNextAutoIndexes(): array { - return $this->nextAutoIndex; + return $this->nextAutoIndexes; } /** @@ -127,14 +231,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; @@ -169,15 +279,6 @@ private function powerSet(array $in): array return $return; } - public function getKeyType(): Type - { - if (count($this->keyTypes) > 1) { - return new UnionType($this->keyTypes); - } - - return parent::getKeyType(); - } - /** * @return array */ @@ -199,20 +300,24 @@ public function isOptionalKey(int $i): bool return in_array($i, $this->optionalKeys, true); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof MixedType && !$type instanceof TemplateMixedType) { + if ($type instanceof CompoundType && !$type instanceof IntersectionType) { return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof self && count($this->keyTypes) === 0) { - return TrinaryLogic::createFromBoolean(count($type->keyTypes) === 0); + return AcceptsResult::createFromBoolean(count($type->keyTypes) === 0); } - $result = TrinaryLogic::createYes(); + $result = AcceptsResult::createYes(); foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; - $hasOffset = $type->hasOffsetValueType($keyType); + $hasOffsetValueType = $type->hasOffsetValueType($keyType); + $hasOffset = new AcceptsResult( + $hasOffsetValueType, + $hasOffsetValueType->yes() || !$type->isConstantArray()->yes() ? [] : [sprintf('Array %s have offset %s.', $hasOffsetValueType->no() ? 'does not' : 'might not', $keyType->describe(VerbosityLevel::value()))], + ); if ($hasOffset->no()) { if ($this->isOptionalKey($i)) { continue; @@ -220,33 +325,52 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $hasOffset; } if ($hasOffset->maybe() && $this->isOptionalKey($i)) { - $hasOffset = TrinaryLogic::createYes(); + $hasOffset = AcceptsResult::createYes(); } $result = $result->and($hasOffset); $otherValueType = $type->getOffsetValueType($keyType); - $acceptsValue = $valueType->accepts($otherValueType, $strictTypes); + $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType); + $acceptsValue = $valueType->accepts($otherValueType, $strictTypes)->decorateReasons( + static fn (string $reason) => sprintf( + 'Offset %s (%s) does not accept type %s: %s', + $keyType->describe(VerbosityLevel::precise()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + $reason, + ), + ); + if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0 && $type->isConstantArray()->yes()) { + $acceptsValue = new AcceptsResult($acceptsValue->result, [ + sprintf( + 'Offset %s (%s) does not accept type %s.', + $keyType->describe(VerbosityLevel::precise()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + ), + ]); + } if ($acceptsValue->no()) { return $acceptsValue; } $result = $result->and($acceptsValue); } - return $result->and($type->isArray()); + $result = $result->and(new AcceptsResult($type->isArray(), [])); + if ($type->isOversizedArray()->yes()) { + if (!$result->no()) { + return AcceptsResult::createYes(); + } + } + + return $result; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { if (count($this->keyTypes) === 0) { - if (count($type->keyTypes) > 0) { - if (count($type->optionalKeys) > 0) { - return TrinaryLogic::createMaybe(); - } - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createYes(); + return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []); } $results = []; @@ -254,35 +378,70 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $hasOffset = $type->hasOffsetValueType($keyType); if ($hasOffset->no()) { if (!$this->isOptionalKey($i)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - $results[] = TrinaryLogic::createMaybe(); + $results[] = IsSuperTypeOfResult::createYes(); continue; + } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) { + $results[] = IsSuperTypeOfResult::createMaybe(); + } + + $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[] = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); + $results[] = $isValueSuperType; } - return TrinaryLogic::createYes()->and(...$results); + return IsSuperTypeOfResult::createYes()->and(...$results); } if ($type instanceof ArrayType) { - $result = TrinaryLogic::createMaybe(); + $result = IsSuperTypeOfResult::createMaybe(); if (count($this->keyTypes) === 0) { return $result; } - return $result->and( - $this->getKeyType()->isSuperTypeOf($type->getKeyType()), - $this->getItemType()->isSuperTypeOf($type->getItemType()) - ); + $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType()); + if ($isKeySuperType->no()) { + return $isKeySuperType; + } + + return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType())); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + + if ($this->isIterableAtLeastOnce()->no()) { + if ($type->isIterableAtLeastOnce()->yes()) { + return new ConstantBooleanType(false); + } + + $constantScalarValues = $type->getConstantScalarValues(); + if (count($constantScalarValues) > 0) { + $results = []; + foreach ($constantScalarValues as $constantScalarValue) { + // @phpstan-ignore equal.invalid, equal.notAllowed + $results[] = TrinaryLogic::createFromBoolean($constantScalarValue == []); // phpcs:ignore + } + + return TrinaryLogic::extremeIdentity(...$results)->toBooleanType(); + } + } + + return new BooleanType(); } public function equals(Type $type): bool @@ -314,103 +473,152 @@ 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; } - public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod + /** @return ConstantArrayTypeAndMethod[] */ + public function findTypeAndMethodNames(): array { if (count($this->keyTypes) !== 2) { - return null; + return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return null; + $classOrObject = null; + $method = null; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) { + $classOrObject = $this->valueTypes[$i]; + continue; + } + + if (!$keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) { + continue; + } + + $method = $this->valueTypes[$i]; } - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return null; + if ($classOrObject === null || $method === null) { + return []; } - [$classOrObject, $method] = $this->valueTypes; + $callableArray = [$classOrObject, $method]; - if (!$method instanceof ConstantStringType) { - return ConstantArrayTypeAndMethod::createUnknown(); + [$classOrObject, $methods] = $callableArray; + if (count($methods->getConstantStrings()) === 0) { + return [ConstantArrayTypeAndMethod::createUnknown()]; } - if ($classOrObject instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($classOrObject->getValue())) { - return ConstantArrayTypeAndMethod::createUnknown(); - } - $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName()); - } elseif ($classOrObject instanceof GenericClassStringType) { - $type = $classOrObject->getGenericType(); - } elseif ((new \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(); + if ($offsetArrayKeyType instanceof ErrorType) { + $allowedArrayKeys = AllowedArrayKeysTypes::getType(); + $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $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(); } @@ -434,16 +642,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) { @@ -453,6 +681,10 @@ public function getOffsetValueType(Type $offsetType): Type return $type; } + if ($maybeAll) { + return $this->getIterableValueType(); + } + return new ErrorType(); // undefined offset } @@ -464,251 +696,762 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $builder->getArray(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($offsetType, $valueType); + + return $builder->getArray(); + } + public function unsetOffset(Type $offsetType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { - $keyTypes = $this->keyTypes; - unset($keyTypes[$i]); - $valueTypes = $this->valueTypes; - unset($valueTypes[$i]); - - $newKeyTypes = []; - $newValueTypes = []; - $newOptionalKeys = []; - - $k = 0; - foreach ($keyTypes as $j => $newKeyType) { - $newKeyTypes[] = $newKeyType; - $newValueTypes[] = $valueTypes[$j]; - if (in_array($j, $this->optionalKeys, true)) { - $newOptionalKeys[] = $k; - } - $k++; + if ($keyType->getValue() !== $offsetType->getValue()) { + continue; + } + + $keyTypes = $this->keyTypes; + unset($keyTypes[$i]); + $valueTypes = $this->valueTypes; + unset($valueTypes[$i]); + + $newKeyTypes = []; + $newValueTypes = []; + $newOptionalKeys = []; + + $k = 0; + foreach ($keyTypes as $j => $newKeyType) { + $newKeyTypes[] = $newKeyType; + $newValueTypes[] = $valueTypes[$j]; + if (in_array($j, $this->optionalKeys, true)) { + $newOptionalKeys[] = $k; + } + $k++; + } + + return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo()); + } + + return $this; + } + + $constantScalars = $offsetType->getConstantScalarTypes(); + if (count($constantScalars) > 0) { + $optionalKeys = $this->optionalKeys; + + foreach ($constantScalars as $constantScalar) { + $constantScalar = $constantScalar->toArrayKey(); + if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) { + continue; + } + + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $constantScalar->getValue()) { + continue; + } + + if (in_array($i, $optionalKeys, true)) { + continue 2; } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndex, $newOptionalKeys); + $optionalKeys[] = $i; } } + + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); } - $arrays = []; - foreach ($this->getAllArrays() as $tmp) { - $arrays[] = new self($tmp->keyTypes, $tmp->valueTypes, $tmp->nextAutoIndex, array_keys($tmp->keyTypes)); + $optionalKeys = $this->optionalKeys; + $isList = $this->isList; + foreach ($this->keyTypes as $i => $keyType) { + if (!$offsetType->isSuperTypeOf($keyType)->yes()) { + continue; + } + $optionalKeys[] = $i; + $isList = TrinaryLogic::createNo(); } + $optionalKeys = array_values(array_unique($optionalKeys)); - return TypeUtils::generalizeType(TypeCombinator::union(...$arrays), GeneralizePrecision::moreSpecific()); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList); } - public function isIterableAtLeastOnce(): TrinaryLogic + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type { - $keysCount = count($this->keyTypes); - if ($keysCount === 0) { - return TrinaryLogic::createNo(); - } + $biggerOne = IntegerRangeType::fromInterval(1, null); + $finiteTypes = $lengthType->getFiniteTypes(); + if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) { + $results = []; + foreach ($finiteTypes as $finiteType) { + if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) { + return $this->traitChunkArray($lengthType, $preserveKeys); + } - $optionalKeysCount = count($this->optionalKeys); - if ($optionalKeysCount < $keysCount) { - return TrinaryLogic::createYes(); + $length = $finiteType->getValue(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $keyTypesCount = count($this->keyTypes); + for ($i = 0; $i < $keyTypesCount; $i += $length) { + $chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes()); + $builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray()); + } + + $results[] = $builder->getArray(); + } + + return TypeCombinator::union(...$results); } - return TrinaryLogic::createMaybe(); + return $this->traitChunkArray($lengthType, $preserveKeys); } - public function removeLast(): self + public function fillKeysArray(Type $valueType): Type { - if (count($this->keyTypes) === 0) { - return $this; + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->valueTypes as $i => $keyType) { + if ($keyType->isInteger()->no()) { + $stringKeyType = $keyType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + } else { + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + } } - $i = count($this->keyTypes) - 1; + return $builder->getArray(); + } - $keyTypes = $this->keyTypes; - $valueTypes = $this->valueTypes; - $optionalKeys = $this->optionalKeys; - unset($optionalKeys[$i]); + public function flipArray(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); - $removedKeyType = array_pop($keyTypes); - array_pop($valueTypes); - $nextAutoindex = $removedKeyType instanceof ConstantIntegerType - ? $removedKeyType->getValue() - : $this->nextAutoIndex; + foreach ($this->keyTypes as $i => $keyType) { + $valueType = $this->valueTypes[$i]; + $builder->setOffsetValueType( + $valueType->toArrayKey(), + $keyType, + $this->isOptionalKey($i), + ); + } - return new self( - $keyTypes, - $valueTypes, - $nextAutoindex, - array_values($optionalKeys) - ); + return $builder->getArray(); } - public function removeFirst(): Type + public function intersectKeyArray(Type $otherArraysType): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($this->keyTypes as $i => $keyType) { - if ($i === 0) { - continue; - } + foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; + $has = $otherArraysType->hasOffsetValueType($keyType); + if ($has->no()) { + continue; } - - $builder->setOffsetValueType($keyType, $valueType); + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes()); } return $builder->getArray(); } - public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self + public function popArray(): Type { - if (count($this->keyTypes) === 0) { - return $this; + return $this->removeLastElements(1); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no() + ? $this->keyTypes[$i] + : null; + $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i)); } - $keyTypes = array_slice($this->keyTypes, $offset, $limit); - $valueTypes = array_slice($this->valueTypes, $offset, $limit); + return $builder->getArray(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + $strict ??= TrinaryLogic::createMaybe(); + $matches = []; + $hasIdenticalValue = false; + + foreach ($this->valueTypes as $index => $valueType) { + if ($strict->yes()) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } + } - if (!$preserveKeys) { - $i = 0; - /** @var array $keyTypes */ - $keyTypes = array_map(static function ($keyType) use (&$i) { - if ($keyType instanceof ConstantIntegerType) { - $i++; - return new ConstantIntegerType($i - 1); + if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType) { + // @phpstan-ignore equal.notAllowed + $isLooseEqual = $needleType->getValue() == $valueType->getValue(); // phpcs:ignore + if (!$isLooseEqual) { + continue; } + if ( + ($strict->no() || $needleType->getValue() === $valueType->getValue()) + && !$this->isOptionalKey($index) + ) { + $hasIdenticalValue = true; + } + } - return $keyType; - }, $keyTypes); + $matches[] = $this->keyTypes[$index]; } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = 0; - foreach ($keyTypes as $keyType) { - if (!$keyType instanceof ConstantIntegerType) { - continue; + if (count($matches) > 0) { + if ($hasIdenticalValue) { + return TypeCombinator::union(...$matches); } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); + return TypeCombinator::union(new ConstantBooleanType(false), ...$matches); } - return new self( - $keyTypes, - $valueTypes, - (int) $nextAutoIndex, - [] - ); - } - - public function toBoolean(): BooleanType - { - return $this->count()->toBoolean(); + return new ConstantBooleanType(false); } - public function toInteger(): Type + public function shiftArray(): Type { - return $this->toBoolean()->toInteger(); + return $this->removeFirstElements(1); } - public function toFloat(): Type + public function shuffleArray(): Type { - return $this->toBoolean()->toFloat(); + return $this->getValuesArray()->degradeToGeneralArray(); } - public function generalize(GeneralizePrecision $precision): Type + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - if (count($this->keyTypes) === 0) { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { return $this; } - $arrayType = new ArrayType( - TypeUtils::generalizeType($this->getKeyType(), $precision), - TypeUtils::generalizeType($this->getItemType(), $precision) - ); + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; - if (count($this->keyTypes) > count($this->optionalKeys)) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; } - return $arrayType; - } + if ($offset === null || $length === null) { + return $this->degradeToGeneralArray() + ->sliceArray($offsetType, $lengthType, $preserveKeys); + } - /** - * @return self - */ - public function generalizeValues(): ArrayType - { - $valueTypes = []; - foreach ($this->valueTypes as $valueType) { - $valueTypes[] = TypeUtils::generalizeType($valueType, GeneralizePrecision::lessSpecific()); + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); - } + if ($keyTypesCount + $length <= 0) { + // A negative length cannot reach left outside the array twice + $length = 0; + } - /** - * @return self - */ - public function getKeysArray(): ArrayType + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type { - $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; + } - foreach ($this->keyTypes as $i => $keyType) { - $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $keyType; - $autoIndex++; + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; - if (!$this->isOptionalKey($i)) { - continue; + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; + } + + if ($offset === null || $length === null) { + return $this->degradeToGeneralArray() + ->spliceArray($offsetType, $lengthType, $replacementType); + } + + 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; + } + + $offsetWasNegative = false; + if ($offset < 0) { + $offsetWasNegative = true; + $offset = $keyTypesCount + $offset; + } + + if ($length < 0) { + $length = $keyTypesCount - $offset + $length; + } + + $extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes()); + + $types = []; + foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) { + $removeKeysCount = 0; + $optionalKeysBeforeReplacement = 0; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0;; $i++) { + $isOptional = $this->isOptionalKey($i); + + if (!$offsetWasNegative && $i < $offset && $isOptional) { + $optionalKeysBeforeReplacement++; + } + + if ($i === $offset + $optionalKeysBeforeReplacement) { + // When the offset is reached we have to a) put the replacement array in and b) remove $length elements + $removeKeysCount = $length; + + if ($replacementArrayType instanceof self) { + $valuesArray = $replacementArrayType->getValuesArray(); + for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) { + $builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j)); + } + } else { + $builder->degradeToGeneralArray(); + $builder->setOffsetValueType($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true); + } + } + + if (!isset($this->keyTypes[$i])) { + break; + } + + if ($removeKeysCount > 0) { + $extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]); + + if ( + (!$isOptional && $extractTypeHasOffsetValueType->yes()) + || ($isOptional && $extractTypeHasOffsetValueType->maybe()) + ) { + $removeKeysCount--; + continue; + } + } + + if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) { + $isOptional = true; + } + + $builder->setOffsetValueType( + $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null, + $this->valueTypes[$i], + $isOptional, + ); } - $optionalKeys[] = $i; + $types[] = $builder->getArray(); } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return TypeCombinator::union(...$types); } - /** - * @return self - */ - public function getValuesArray(): ArrayType + 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 = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; + 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) { - $keyTypes[] = new ConstantIntegerType($i); $valueTypes[] = $valueType; - $autoIndex++; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + public function getLastIterableValueType(): Type + { + $valueTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $valueTypes[] = $this->valueTypes[$i]; if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + return $this->isList; + } + + /** @param positive-int $length */ + private function removeLastElements(int $length): self + { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; + } + + $keyTypes = $this->keyTypes; + $valueTypes = $this->valueTypes; + $optionalKeys = $this->optionalKeys; + $nextAutoindexes = $this->nextAutoIndexes; + + $optionalKeysRemoved = 0; + $newLength = $keyTypesCount - $length; + for ($i = $keyTypesCount - 1; $i >= 0; $i--) { + $isOptional = $this->isOptionalKey($i); + + if ($i >= $newLength) { + if ($isOptional) { + $optionalKeysRemoved++; + foreach ($optionalKeys as $key => $value) { + if ($value === $i) { + unset($optionalKeys[$key]); + break; + } + } + } + + $removedKeyType = array_pop($keyTypes); + array_pop($valueTypes); + $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType + ? [$removedKeyType->getValue()] + : $this->nextAutoIndexes; + continue; + } + + if ($isOptional || $optionalKeysRemoved <= 0) { continue; } $optionalKeys[] = $i; + $optionalKeysRemoved--; } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + 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 count(): Type + public function generalize(GeneralizePrecision $precision): Type { + if (count($this->keyTypes) === 0) { + return $this; + } + + if ($precision->isTemplateArgument()) { + return $this->traverse(static fn (Type $type) => $type->generalize($precision)); + } + + $arrayType = new ArrayType( + $this->getIterableKeyType()->generalize($precision), + $this->getIterableValueType()->generalize($precision), + ); + + $keyTypesCount = count($this->keyTypes); $optionalKeysCount = count($this->optionalKeys); - $totalKeysCount = count($this->getKeyTypes()); - if ($optionalKeysCount === 0) { - return new ConstantIntegerType($totalKeysCount); + + $accessoryTypes = []; + if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) { + foreach ($this->keyTypes as $i => $keyType) { + if ($this->isOptionalKey($i)) { + continue; + } + + $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision)); + } + } elseif ($keyTypesCount > $optionalKeysCount) { + $accessoryTypes[] = new NonEmptyArrayType(); } - return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount); + if ($this->isList()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + if (count($accessoryTypes) > 0) { + return TypeCombinator::intersect($arrayType, ...$accessoryTypes); + } + + return $arrayType; + } + + public function generalizeValues(): self + { + $valueTypes = []; + foreach ($this->valueTypes as $valueType) { + $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); + } + + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + private function degradeToGeneralArray(): Type + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->degradeToGeneralArray(); + + return $builder->getArray(); + } + + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + $keysArray = $this->getKeysOrValuesArray($this->keyTypes); + + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + $keysArray->getIterableValueType(), + ), + new AccessoryArrayListType(), + ); + } + + 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 @@ -728,8 +1471,20 @@ 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); + } elseif (!self::isValidIdentifier($keyDescription)) { + $keyDescription = sprintf('\'%s\'', $keyDescription); + } + } + + $valueTypeDescription = $valueType->describe($level); + $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription); + $values[] = $valueTypeDescription; } $append = ''; @@ -740,21 +1495,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), ); } @@ -778,12 +1527,19 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $typeMap; } - return parent::inferTemplateTypes($receivedType); + if ($receivedType->isArray()->yes()) { + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $itemTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType()); + + return $keyTypeMap->union($itemTypeMap); + } + + return TemplateTypeMap::createEmpty(); } public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $variance = $positionVariance->compose(TemplateTypeVariance::createInvariant()); + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); $references = []; foreach ($this->keyTypes as $type) { @@ -801,6 +1557,27 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return $references; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) { + return TypeCombinator::intersect($this, new NonEmptyArrayType()); + } + + if ($typeToRemove instanceof NonEmptyArrayType) { + return new ConstantArrayType([], []); + } + + if ($typeToRemove instanceof HasOffsetType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + if ($typeToRemove instanceof HasOffsetValueType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + return null; + } + public function traverse(callable $cb): Type { $valueTypes = []; @@ -819,37 +1596,90 @@ public function traverse(callable $cb): Type return $this; } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } - public function isKeysSupersetOf(self $otherArray): bool + public function traverseSimultaneously(Type $right, callable $cb): Type { - if (count($this->keyTypes) === 0) { - return count($otherArray->keyTypes) === 0; + if (!$right->isArray()->yes()) { + return $this; } - if (count($otherArray->keyTypes) === 0) { + $valueTypes = []; + + $stillOriginal = true; + foreach ($this->valueTypes as $i => $valueType) { + $keyType = $this->keyTypes[$i]; + $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType)); + if ($transformedValueType !== $valueType) { + $stillOriginal = false; + } + + $valueTypes[] = $transformedValueType; + } + + if ($stillOriginal) { + return $this; + } + + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + public function isKeysSupersetOf(self $otherArray): bool + { + $keyTypesCount = count($this->keyTypes); + $otherKeyTypesCount = count($otherArray->keyTypes); + + if ($keyTypesCount < $otherKeyTypesCount) { return false; } - $otherKeys = $otherArray->keyTypes; - foreach ($this->keyTypes as $keyType) { - foreach ($otherArray->keyTypes as $j => $otherKeyType) { - if (!$keyType->equals($otherKeyType)) { - continue; - } + if ($otherKeyTypesCount === 0) { + return $keyTypesCount === 0; + } + + $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2; + + $keyTypes = $this->keyTypes; + + foreach ($otherArray->keyTypes as $j => $keyType) { + $i = self::findKeyIndex($keyType, $keyTypes); + if ($i === null) { + return false; + } + + unset($keyTypes[$i]); + + $valueType = $this->valueTypes[$i]; + $otherValueType = $otherArray->valueTypes[$j]; + if (!$otherValueType->isSuperTypeOf($valueType)->no()) { + continue; + } + + if ($failOnDifferentValueType) { + return false; + } + $failOnDifferentValueType = true; + } + + $requiredKeyCount = 0; + foreach (array_keys($keyTypes) as $i) { + if ($this->isOptionalKey($i)) { + continue; + } - unset($otherKeys[$j]); - continue 2; + $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) { @@ -867,16 +1697,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; } @@ -887,7 +1728,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)) { @@ -897,7 +1738,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); } } @@ -907,13 +1748,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 { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); + $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 + { + $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 3bc1809ea8..07f4156550 100644 --- a/src/Type/Constant/ConstantArrayTypeAndMethod.php +++ b/src/Type/Constant/ConstantArrayTypeAndMethod.php @@ -2,38 +2,32 @@ namespace PHPStan\Type\Constant; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ -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); } @@ -51,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; @@ -60,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 aebd713e9e..a639bf6c0e 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -2,115 +2,288 @@ 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\GeneralizePrecision; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_filter; +use function array_map; +use function array_unique; +use function array_values; +use function count; +use function in_array; +use function is_float; +use function max; +use function min; +use function range; -/** @api */ -class ConstantArrayTypeBuilder +/** + * @api + */ +final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; - /** @var array */ - private array $keyTypes; - - /** @var array */ - private array $valueTypes; - - /** @var array */ - private array $optionalKeys; - - private int $nextAutoIndex; - 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; + } + + $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType); + $offsetMatch = true; + } + + if ($offsetMatch) { + continue; + } + + $match = false; + } - /** @var int|float $newNextAutoIndex */ - $newNextAutoIndex = $offsetType instanceof ConstantIntegerType - ? max($this->nextAutoIndex, $offsetType->getValue() + 1) - : $this->nextAutoIndex; - if (!is_float($newNextAutoIndex)) { - $this->nextAutoIndex = $newNextAutoIndex; + 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[] = TypeUtils::generalizeType($offsetType, GeneralizePrecision::moreSpecific()); + $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(): Type @@ -123,19 +296,32 @@ public function getArray(): Type if (!$this->degradeToGeneralArray) { /** @var array $keyTypes */ $keyTypes = $this->keyTypes; - return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } $array = new ArrayType( TypeCombinator::union(...$this->keyTypes), - TypeCombinator::union(...$this->valueTypes) + TypeCombinator::union(...$this->valueTypes), ); if (count($this->optionalKeys) < $keyTypesCount) { - return TypeCombinator::intersect($array, new NonEmptyArrayType()); + $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); + } + + if ($this->oversized) { + $array = TypeCombinator::intersect($array, new OversizedArrayType()); + } + + if ($this->isList->yes()) { + $array = TypeCombinator::intersect($array, new AccessoryArrayListType()); } return $array; } + public function isList(): bool + { + return $this->isList->yes(); + } + } diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 30d156eac1..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -2,28 +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; + } /** @api */ - public function __construct(bool $value) + public function __construct(private bool $value) { parent::__construct(); - $this->value = $value; } public function getValue(): bool @@ -36,7 +41,7 @@ public function describe(VerbosityLevel $level): string return $this->value ? 'true' : 'false'; } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { if ($this->value) { return StaticTypeFactory::falsey(); @@ -44,7 +49,7 @@ public function getSmallerType(): Type return new NeverType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { if ($this->value) { return new MixedType(); @@ -52,7 +57,7 @@ public function getSmallerOrEqualType(): Type return StaticTypeFactory::falsey(); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { if ($this->value) { return new NeverType(); @@ -60,7 +65,7 @@ public function getGreaterType(): Type return StaticTypeFactory::truthy(); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { if ($this->value) { return StaticTypeFactory::truthy(); @@ -78,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); @@ -93,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 ConstantIntegerType((int) $this->value); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + + return $this; + } + + public function isTrue(): TrinaryLogic { - return new self($properties['value']); + return TrinaryLogic::createFromBoolean($this->value === true); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->value === false); + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new BooleanType(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->value ? 'true' : 'false'); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isObject()->yes()) { + return $this; + } + + return $this->scalarLooseCompare($type, $phpVersion); } } diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 55bb56cd1e..b0cafcdca3 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -2,14 +2,23 @@ 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\UnionType; 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 @@ -19,13 +28,10 @@ class ConstantFloatType extends FloatType implements ConstantScalarType use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; - private float $value; - /** @api */ - public function __construct(float $value) + public function __construct(private float $value) { parent::__construct(); - $this->value = $value; } public function getValue(): float @@ -33,50 +39,48 @@ 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 { + if (is_nan($value)) { + return 'NAN'; } - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } + $valueStr = (string) $value; + if (is_finite($value) && !str_contains($valueStr, '.')) { + $valueStr .= '.0'; + } - 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 { + if ($this->value === 0.0) { + return new UnionType([ + new ConstantStringType('0'), + new ConstantStringType('-0'), + ]); + } + return new ConstantStringType((string) $this->value); } @@ -85,13 +89,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 5f5c57eddc..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -2,15 +2,22 @@ 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 @@ -20,13 +27,10 @@ class ConstantIntegerType extends IntegerType implements ConstantScalarType use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; - private int $value; - /** @api */ - public function __construct(int $value) + public function __construct(private int $value) { parent::__construct(); - $this->value = $value; } public function getValue(): int @@ -34,43 +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) { $min = $type->getMin(); $max = $type->getMax(); if (($min === null || $min <= $this->value) && ($max === null || $this->value <= $max)) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function describe(VerbosityLevel $level): string { 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), ); } @@ -79,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 b57e467e25..e4c34e609f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -2,14 +2,29 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Name; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; +use PHPStan\Reflection\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; @@ -19,6 +34,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -28,6 +44,17 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function addcslashes; +use function in_array; +use function is_float; +use function is_int; +use function is_numeric; +use function key; +use function strlen; +use function strtolower; +use function strtoupper; +use function substr; +use function substr_count; /** @api */ class ConstantStringType extends StringType implements ConstantScalarType @@ -38,16 +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; /** @api */ - public function __construct(string $value, bool $isClassString = false) + public function __construct(private string $value, private bool $isClassString = false) { parent::__construct(); - $this->value = $value; - $this->isClassString = $isClassString; } public function getValue(): string @@ -55,45 +80,73 @@ public function getValue(): string return $this->value; } - public function isClassString(): bool + public function getConstantStrings(): array { - return $this->isClassString; + 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}"; + } } - try { - $truncatedValue = \Nette\Utils\Strings::truncate($this->value, self::DESCRIBE_LIMIT); - } catch (\Nette\Utils\RegexpException $e) { - $truncatedValue = substr($this->value, 0, self::DESCRIBE_LIMIT) . "\u{2026}"; - } - - return var_export( - $truncatedValue, - 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(); @@ -101,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. @@ -113,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) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - - return $reflectionProvider->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 @@ -152,18 +203,27 @@ public function isCallable(): TrinaryLogic } // '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 (!$reflectionProvider->hasClass($matches[1])) { return TrinaryLogic::createMaybe(); } + $phpVersion = PhpVersionStaticAccessor::getInstance(); $classRef = $reflectionProvider->getClass($matches[1]); if ($classRef->hasMethod($matches[2])) { + $method = $classRef->getMethod($matches[2], new OutOfClassScope()); + if ( + !$phpVersion->supportsCallableInstanceMethods() + && !$method->isStatic() + ) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createYes(); } - if (!$classRef->getNativeReflection()->isFinal()) { + if (!$classRef->isFinalByKeyword()) { return TrinaryLogic::createMaybe(); } @@ -173,22 +233,23 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + if ($this->value === '') { + return []; + } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' $functionName = new Name($this->value); if ($reflectionProvider->hasFunction($functionName, null)) { - return $reflectionProvider->getFunction($functionName, null)->getVariants(); + $function = $reflectionProvider->getFunction($functionName, null); + return FunctionCallableVariant::createFromVariants($function, $function->getVariants()); } // 'MyClass::myStaticFunction' - $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 (!$reflectionProvider->hasClass($matches[1])) { return [new TrivialParametersAcceptor()]; @@ -201,21 +262,20 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new InaccessibleMethod($method)]; } - return $method->getVariants(); + return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } } - 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)) { @@ -228,6 +288,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new ConstantIntegerType((int) $this->value); @@ -238,6 +303,22 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } + public function toArrayKey(): Type + { + if ($this->arrayKeyType !== null) { + return $this->arrayKeyType; + } + + /** @var int|string $offsetValue */ + $offsetValue = key([$this->value => null]); + return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createFromBoolean(is_numeric($this->getValue())); @@ -248,17 +329,32 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->getValue() !== ''); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true)); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtoupper($this->value) === $this->value); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($offsetType instanceof ConstantIntegerType) { - return TrinaryLogic::createFromBoolean( - $offsetType->getValue() < strlen($this->value) - ); + if ($offsetType->isInteger()->yes()) { + $strlen = strlen($this->value); + $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1); + return $strLenType->isSuperTypeOf($offsetType)->result; } return parent::hasOffsetValueType($offsetType); @@ -266,12 +362,35 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - if ($offsetType instanceof ConstantIntegerType) { - if ($offsetType->getValue() < strlen($this->value)) { - return new self($this->value[$offsetType->getValue()]); + if ($offsetType->isInteger()->yes()) { + $strlen = strlen($this->value); + $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1); + + if ($offsetType instanceof ConstantIntegerType) { + if ($strLenType->isSuperTypeOf($offsetType)->yes()) { + return new self($this->value[$offsetType->getValue()]); + } + + return new ErrorType(); } - return new ErrorType(); + $intersected = TypeCombinator::intersect($strLenType, $offsetType); + if ($intersected instanceof IntegerRangeType) { + $finiteTypes = $intersected->getFiniteTypes(); + if ($finiteTypes === []) { + return parent::getOffsetValueType($offsetType); + } + + $chars = []; + foreach ($finiteTypes as $constantInteger) { + $chars[] = new self($this->value[$constantInteger->getValue()]); + } + if (!$strLenType->isSuperTypeOf($offsetType)->yes()) { + $chars[] = new self(''); + } + + return TypeCombinator::union(...$chars); + } } return parent::getOffsetValueType($offsetType); @@ -288,7 +407,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni && $valueStringType instanceof ConstantStringType ) { $value = $this->value; - $value[$offsetType->getValue()] = $valueStringType->getValue(); + $offsetValue = $offsetType->getValue(); + if ($offsetValue < 0) { + return new ErrorType(); + } + $stringValue = $valueStringType->getValue(); + if (strlen($stringValue) !== 1) { + return new ErrorType(); + } + $value[$offsetValue] = $stringValue; return new self($value); } @@ -296,6 +423,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return parent::setOffsetValueType($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return parent::setOffsetValueType($offsetType, $valueType); + } + public function append(self $otherString): self { return new self($this->getValue() . $otherString->getValue()); @@ -312,11 +444,30 @@ public function generalize(GeneralizePrecision $precision): Type } if ($this->getValue() !== '' && $precision->isMoreSpecific()) { - return new IntersectionType([ + $accessories = [ new StringType(), - new AccessoryNonEmptyStringType(), new AccessoryLiteralStringType(), - ]); + ]; + + if (is_numeric($this->getValue())) { + $accessories[] = new AccessoryNumericStringType(); + } + + if ($this->getValue() !== '0') { + $accessories[] = new AccessoryNonFalsyStringType(); + } else { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if (strtolower($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + + if (strtoupper($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($accessories); } if ($precision->isMoreSpecific()) { @@ -329,7 +480,7 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -348,7 +499,7 @@ public function getSmallerType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllGreaterThan((float) $this->value), @@ -361,7 +512,7 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(false), @@ -375,7 +526,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllSmallerThan((float) $this->value), @@ -388,13 +539,33 @@ public function getGreaterOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function canAccessConstants(): TrinaryLogic { - return new self($properties['value'], $properties['isClassString'] ?? false); + 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..530fe86046 --- /dev/null +++ b/src/Type/Constant/OversizedArrayBuilder.php @@ -0,0 +1,103 @@ +items; + for ($i = 0; $i < count($items); $i++) { + $item = $items[$i]; + if (!$item->unpack) { + continue; + } + + $valueType = $getTypeCallback($item->value); + if ($valueType instanceof ConstantArrayType) { + array_splice($items, $i, 1); + foreach ($valueType->getKeyTypes() as $j => $innerKeyType) { + $innerValueType = $valueType->getValueTypes()[$j]; + if ($innerKeyType->isString()->no()) { + $keyExpr = null; + } else { + $keyExpr = new TypeExpr($innerKeyType); + } + array_splice($items, $i++, 0, [new ArrayItem( + new TypeExpr($innerValueType), + $keyExpr, + )]); + } + } else { + array_splice($items, $i, 1, [new ArrayItem( + new TypeExpr($valueType->getIterableValueType()), + new TypeExpr($valueType->getIterableKeyType()), + )]); + } + } + foreach ($items as $item) { + if ($item->unpack) { + throw new ShouldNotHappenException(); + } + if ($item->key !== null) { + $itemKeyType = $getTypeCallback($item->key); + if (!$itemKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($itemKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $itemKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } + } else { + $itemKeyType = new ConstantIntegerType($nextAutoIndex); + $nextAutoIndex++; + } + + $generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; + + $itemValueType = $getTypeCallback($item->value); + $generalizedValueType = $itemValueType->generalize(GeneralizePrecision::moreSpecific()); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } + + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + } + +} diff --git a/src/Type/ConstantScalarType.php b/src/Type/ConstantScalarType.php index f44e18333b..b84b381717 100644 --- a/src/Type/ConstantScalarType.php +++ b/src/Type/ConstantScalarType.php @@ -3,7 +3,7 @@ namespace PHPStan\Type; /** @api */ -interface ConstantScalarType extends ConstantType +interface ConstantScalarType extends Type { /** diff --git a/src/Type/ConstantType.php b/src/Type/ConstantType.php deleted file mode 100644 index 0f08d47c81..0000000000 --- a/src/Type/ConstantType.php +++ /dev/null @@ -1,11 +0,0 @@ - ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); + $arrayBuilder->degradeToGeneralArray(true); } foreach ($value as $k => $v) { $arrayBuilder->setOffsetValueType(self::getTypeFromValue($k), self::getTypeFromValue($v)); } 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 ac4750ce53..eb7b6222ff 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -6,12 +6,28 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.broker.dynamicFunctionReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicFunctionReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool; - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type; + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicFunctionThrowTypeExtension.php b/src/Type/DynamicFunctionThrowTypeExtension.php index 46f56ce61b..9e16865c3c 100644 --- a/src/Type/DynamicFunctionThrowTypeExtension.php +++ b/src/Type/DynamicFunctionThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.dynamicFunctionThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicFunctionThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicFunctionThrowTypeExtension { diff --git a/src/Type/DynamicMethodReturnTypeExtension.php b/src/Type/DynamicMethodReturnTypeExtension.php index 58635acca7..e8af9137b0 100644 --- a/src/Type/DynamicMethodReturnTypeExtension.php +++ b/src/Type/DynamicMethodReturnTypeExtension.php @@ -6,14 +6,31 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicMethodReturnTypeExtension { + /** @return class-string */ public function getClass(): string; public function isMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type; + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicMethodThrowTypeExtension.php b/src/Type/DynamicMethodThrowTypeExtension.php index e6fba9fc7f..228604cb83 100644 --- a/src/Type/DynamicMethodThrowTypeExtension.php +++ b/src/Type/DynamicMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.dynamicMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicMethodThrowTypeExtension { diff --git a/src/Type/DynamicReturnTypeExtensionRegistry.php b/src/Type/DynamicReturnTypeExtensionRegistry.php index 584c078658..e2a30437b3 100644 --- a/src/Type/DynamicReturnTypeExtensionRegistry.php +++ b/src/Type/DynamicReturnTypeExtensionRegistry.php @@ -2,69 +2,42 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\ReflectionProvider; +use function array_merge; +use function strtolower; -class DynamicReturnTypeExtensionRegistry +final class DynamicReturnTypeExtensionRegistry { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - /** @var \PHPStan\Type\DynamicMethodReturnTypeExtension[] */ - private array $dynamicMethodReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] */ - private array $dynamicStaticMethodReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicFunctionReturnTypeExtension[] */ - private array $dynamicFunctionReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicMethodReturnTypeExtension[][]|null */ + /** @var DynamicMethodReturnTypeExtension[][]|null */ private ?array $dynamicMethodReturnTypeExtensionsByClass = null; - /** @var \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[][]|null */ + /** @var DynamicStaticMethodReturnTypeExtension[][]|null */ private ?array $dynamicStaticMethodReturnTypeExtensionsByClass = null; /** - * @param \PHPStan\Broker\Broker $broker - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions + * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions + * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions + * @param DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions */ public function __construct( - Broker $broker, - ReflectionProvider $reflectionProvider, - array $dynamicMethodReturnTypeExtensions, - array $dynamicStaticMethodReturnTypeExtensions, - array $dynamicFunctionReturnTypeExtensions + private ReflectionProvider $reflectionProvider, + private array $dynamicMethodReturnTypeExtensions, + private array $dynamicStaticMethodReturnTypeExtensions, + private array $dynamicFunctionReturnTypeExtensions, ) { - foreach (array_merge($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions, $dynamicFunctionReturnTypeExtensions) as $extension) { - if (!($extension instanceof BrokerAwareExtension)) { - continue; - } - - $extension->setBroker($broker); - } - - $this->reflectionProvider = $reflectionProvider; - $this->dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions; - $this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions; - $this->dynamicFunctionReturnTypeExtensions = $dynamicFunctionReturnTypeExtensions; } /** - * @param string $className - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] + * @return DynamicMethodReturnTypeExtension[] */ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array { 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 e2de530f9f..e74f69460f 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -6,14 +6,31 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicStaticMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicStaticMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicStaticMethodReturnTypeExtension { + /** @return class-string */ public function getClass(): string; public function isStaticMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type; + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicStaticMethodThrowTypeExtension.php b/src/Type/DynamicStaticMethodThrowTypeExtension.php index b01735a6bd..fa9926dea3 100644 --- a/src/Type/DynamicStaticMethodThrowTypeExtension.php +++ b/src/Type/DynamicStaticMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.dynamicStaticMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicStaticMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicStaticMethodThrowTypeExtension { diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php new file mode 100644 index 0000000000..ab1091846a --- /dev/null +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -0,0 +1,230 @@ +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 + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return parent::getUnresolvedInstancePropertyPrototype($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::getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + 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 9eb2e2d10a..751271aef3 100644 --- a/src/Type/ErrorType.php +++ b/src/Type/ErrorType.php @@ -15,15 +15,9 @@ public function __construct() public function describe(VerbosityLevel $level): string { return $level->handle( - function () use ($level): string { - return parent::describe($level); - }, - function () use ($level): string { - return parent::describe($level); - }, - static function (): string { - return '*ERROR*'; - } + fn (): string => parent::describe($level), + fn (): string => parent::describe($level), + static fn (): string => '*ERROR*', ); } @@ -47,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 85344d851f..2ce66f1e1a 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -2,154 +2,177 @@ namespace PHPStan\Type; -use PhpParser\Comment\Doc; +use Closure; use PhpParser\Node; use PHPStan\Analyser\NameScope; +use PHPStan\BetterReflection\Util\GetLastDocComment; use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +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\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use 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; + +#[AutowiredService] +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 = []; - - /** @var array */ - private array $docKeys = []; + private int $resolvedPhpDocBlockCacheCount = 0; public function __construct( - ReflectionProviderProvider $reflectionProviderProvider, - Parser $phpParser, - PhpDocStringResolver $phpDocStringResolver, - PhpDocNodeResolver $phpDocNodeResolver, - Cache $cache, - AnonymousClassNameHelper $anonymousClassNameHelper + private ReflectionProviderProvider $reflectionProviderProvider, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] + 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(); } - $phpDocKey = $this->getPhpDocKey($fileName, $className, $traitName, $functionName, $docComment); + if ($docComment === '') { + return ResolvedPhpDocBlock::createEmpty(); + } + + if ($fileName !== null) { + $fileName = $this->fileHelper->normalizePath($fileName); + } + + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); + $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment)); if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) { return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - $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(); } - 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() - )) - ); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap( - new TemplateTypeMap(array_merge( - $nameScope->getTemplateTypeMap()->getTypes(), - $templateTypeMap->getTypes() - )) + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); + if ($this->resolvedPhpDocBlockCacheCount >= 2048) { + $this->resolvedPhpDocBlockCache = array_slice( + $this->resolvedPhpDocBlockCache, + 1, + preserve_keys: 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( @@ -157,124 +180,273 @@ 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) { - $unserializeResult = @unserialize($phpDocNodeSerializedString); - if ($unserializeResult === false) { - $error = error_get_last(); - if ($error !== null) { - throw new \PHPStan\ShouldNotHappenException(sprintf('unserialize() error: %s', $error['message'])); - } - - throw new \PHPStan\ShouldNotHappenException('Unknown unserialize() error'); - } - - return $unserializeResult; - } - - $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); - if ($this->shouldPhpDocNodeBeCachedToDisk($phpDocNode)) { - $this->cache->save($cacheKey, $phpDocParserVersion, serialize($phpDocNode)); - } - - return $phpDocNode; - } - - private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool - { - foreach ($phpDocNode->getTags() as $phpDocTag) { - if (!$phpDocTag->value instanceof InvalidTagValueNode) { - continue; - } - - return false; - } - - return true; - } - /** - * @param string $fileName - * @return \PHPStan\PhpDoc\NameScopedPhpDocString[] + * @return NameScope[] */ - private function getResolvedPhpDocMap(string $fileName): array + private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { - $cacheKey = sprintf('%s-phpdocstring-v10-function-name-stack', $fileName); - $variableCacheKey = implode(',', array_map(static function (array $file): string { - return sprintf('%s-%d', $file['filename'], $file['modifiedTime']); - }, $this->getCachedDependentFilesWithTimestamps($fileName))); - $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, + preserve_keys: 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 array $traitMethodAliases + * @return array + */ + private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array + { + /** @var array $phpDocNodeMap */ + $phpDocNodeMap = []; + + /** @var string[] $classStack */ + $classStack = []; + if ($lookForTrait !== null && $traitUseClass !== null) { + $classStack[] = $traitUseClass; + } + $namespace = null; + + $traitFound = false; + + /** @var array $functionStack */ + $functionStack = []; + $this->processNodes( + $this->phpParser->parseFile($fileName), + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$phpDocNodeMap, &$classStack, &$namespace, &$functionStack): ?int { + if ($node instanceof Node\Stmt\ClassLike) { + if ($traitFound && $fileName === $originalClassFileName) { + return self::SKIP_NODE; + } + + if ($lookForTrait !== null && !$traitFound) { + if (!$node instanceof Node\Stmt\Trait_) { + return self::SKIP_NODE; + } + if ((string) $node->namespacedName !== $lookForTrait) { + return self::SKIP_NODE; + } + + $traitFound = true; + $functionStack[] = null; + } else { + if ($node->name === null) { + if (!$node instanceof Node\Stmt\Class_) { + throw new ShouldNotHappenException(); + } + + $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); + } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) { + $className = $node->name->name; + } else { + if ($traitFound) { + return self::SKIP_NODE; + } + $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } + $classStack[] = $className; + $functionStack[] = null; + } + } elseif ($node instanceof Node\Stmt\ClassMethod) { + if (array_key_exists($node->name->name, $traitMethodAliases)) { + $functionStack[] = $traitMethodAliases[$node->name->name]; + } else { + $functionStack[] = $node->name->name; + } + } elseif ($node instanceof Node\Stmt\Function_) { + $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } + } + + $className = $classStack[count($classStack) - 1] ?? null; + $functionName = $functionStack[count($functionStack) - 1] ?? null; + + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + $docComment = GetLastDocComment::forNode($node); + if ($docComment !== null) { + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); + $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment); + } + + return null; + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $docComment = GetLastDocComment::forNode($node); + if ($docComment !== null) { + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); + $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment); + } + } + + return null; + } + + if ($node instanceof Node\Stmt\Namespace_) { + $namespace = $node->name !== null ? (string) $node->name : null; + } elseif ($node instanceof Node\Stmt\TraitUse) { + $traitMethodAliases = []; + foreach ($node->adaptations as $traitUseAdaptation) { + if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + if ($traitUseAdaptation->newName === null) { + continue; + } + + $methodName = $traitUseAdaptation->method->toString(); + $newTraitName = $traitUseAdaptation->newName->toString(); + + if ($traitUseAdaptation->trait === null) { + foreach ($node->traits as $traitName) { + $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName; + } + continue; + } + + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName; + } + + foreach ($node->traits as $traitName) { + /** @var class-string $traitName */ + $traitName = (string) $traitName; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($traitName)) { + continue; + } + + $traitReflection = $reflectionProvider->getClass($traitName); + if (!$traitReflection->isTrait()) { + continue; + } + if ($traitReflection->getFileName() === null) { + continue; + } + if (!is_file($traitReflection->getFileName())) { + continue; + } + + $className = $classStack[count($classStack) - 1] ?? null; + if ($className === null) { + throw new ShouldNotHappenException(); + } + + $phpDocNodeMap = array_merge($phpDocNodeMap, $this->createPhpDocNodeMap( + $traitReflection->getFileName(), + $traitName, + $className, + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + )); + } + } + + return null; + }, + static function (Node $node) use (&$namespace, &$functionStack, &$classStack): void { + if ($node instanceof Node\Stmt\ClassLike) { + if (count($classStack) === 0) { + throw new ShouldNotHappenException(); + } + array_pop($classStack); + + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\Stmt\Namespace_) { + $namespace = null; + } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } + } + }, + ); + + return $phpDocNodeMap; } /** - * @param string $fileName - * @param string|null $lookForTrait - * @param string|null $traitUseClass * @param array $traitMethodAliases - * @return (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] + * @param array $phpDocNodeMap + * @return (callable(): NameScope)[] */ - private function createFilePhpDocMap( + private function createNameScopeMap( string $fileName, ?string $lookForTrait, ?string $traitUseClass, - array $traitMethodAliases = [] + array $traitMethodAliases, + string $originalClassFileName, + array $phpDocNodeMap, ): array { - /** @var (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] $phpDocMap */ - $phpDocMap = []; + /** @var (callable(): NameScope)[] $nameScopeMap */ + $nameScopeMap = []; /** @var (callable(): TemplateTypeMap)[] $typeMapStack */ $typeMapStack = []; @@ -290,142 +462,166 @@ private function createFilePhpDocMap( } $namespace = null; + $traitFound = false; + /** @var array $functionStack */ $functionStack = []; $uses = []; + $constUses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAliases, &$phpDocMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack): ?int { - $resolvableTemplateTypes = false; + function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?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; + $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 \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; - $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment()); + $classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null); + if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]); + } else { + $typeAliasStack[] = []; + } $functionStack[] = null; - $resolvableTemplateTypes = true; } - } elseif ($node instanceof Node\Stmt\TraitUse) { - $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\ClassMethod) { if (array_key_exists($node->name->name, $traitMethodAliases)) { $functionStack[] = $traitMethodAliases[$node->name->name]; } else { $functionStack[] = $node->name->name; } - $resolvableTemplateTypes = true; - } elseif ( - $node instanceof Node\Param - && $node->flags !== 0 - ) { - $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); - $resolvableTemplateTypes = true; - } elseif ($node instanceof Node\Stmt\Property) { - $resolvableTemplateTypes = true; - } elseif ( - !$node instanceof Node\Stmt - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - ) { - return null; + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } - foreach (array_reverse($node->getComments()) as $comment) { - if (!$comment instanceof Doc) { - continue; + $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, constUses: $constUses, typeAliasClassName: $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(), + )); + }; } + } - $phpDocString = $comment->getText(); - $className = $classStack[count($classStack) - 1] ?? null; - $functionName = $functionStack[count($functionStack) - 1] ?? null; - $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; - $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - - $phpDocKey = $this->getPhpDocKey($fileName, $className, $lookForTrait, $functionName, $phpDocString); - $phpDocMap[$phpDocKey] = static function () use ($phpDocString, $namespace, $uses, $className, $functionName, $typeMapCb, $typeAliasesMap, $resolvableTemplateTypes): NameScopedPhpDocString { - $nameScope = new NameScope( - $namespace, - $uses, - $className, - $functionName, - ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty())->map(static function (string $name, Type $type) use ($className, $resolvableTemplateTypes): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($className, $resolvableTemplateTypes): Type { - if (!$type instanceof TemplateType) { - return $traverse($type); - } - - if (!$resolvableTemplateTypes) { - return $traverse($type->toArgument()); - } - - $scope = $type->getScope(); - - if ($scope->getClassName() === null || $scope->getFunctionName() !== null || $scope->getClassName() !== $className) { - return $traverse($type->toArgument()); - } - - return $traverse($type); - }); - }), - $typeAliasesMap - ); - return new NameScopedPhpDocString($phpDocString, $nameScope); - }; + $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; + $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; + + 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()), + $typeAliasesMap, + constUses: $constUses, + typeAliasClassName: $lookForTrait, + ); + } - if (!($node instanceof Node\Stmt\ClassLike) && !($node instanceof Node\FunctionLike)) { - continue; + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + // property hook skipped on purpose, it does not support @template + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { + return self::POP_TYPE_MAP_STACK; } - $typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap { - $resolvedPhpDoc = $this->getResolvedPhpDoc( - $fileName, - $className, - $lookForTrait, - $functionName, - $phpDocString - ); - return new TemplateTypeMap(array_merge( - $typeMapCb !== null ? $typeMapCb()->getTypes() : [], - $resolvedPhpDoc->getTemplateTypeMap()->getTypes() - )); - }; - - return self::POP_TYPE_MAP_STACK; + return null; } - if ($node instanceof \PhpParser\Node\Stmt\Namespace_) { - $namespace = (string) $node->name; - } 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; + 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 \PhpParser\Node\Stmt\GroupUse) { + } elseif ($node instanceof Node\Stmt\GroupUse) { $prefix = (string) $node->prefix; foreach ($node->uses as $use) { - if ($node->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL && $use->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) { - continue; + if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) { + $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); + } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) { + $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } - - $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } } elseif ($node instanceof Node\Stmt\TraitUse) { $traitMethodAliases = []; @@ -434,15 +630,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; } $useDocComment = null; @@ -465,25 +667,27 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia if ($traitReflection->getFileName() === null) { continue; } - if (!file_exists($traitReflection->getFileName())) { + if (!is_file($traitReflection->getFileName())) { continue; } $className = $classStack[count($classStack) - 1] ?? null; if ($className === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $traitPhpDocMap = $this->createFilePhpDocMap( + $traitPhpDocMap = $this->createNameScopeMap( $traitReflection->getFileName(), $traitName, $className, - $traitMethodAliases[$traitName] ?? [] + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + $phpDocNodeMap, ); $finalTraitPhpDocMap = []; - foreach ($traitPhpDocMap as $phpDocKey => $callback) { - $finalTraitPhpDocMap[$phpDocKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScopedPhpDocString { - /** @var NameScopedPhpDocString $original */ + foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) { + $finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope { + /** @var NameScope $original */ $original = $callback(); if (!$traitReflection->isGeneric()) { return $original; @@ -498,7 +702,7 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia $className, $lookForTrait, null, - $useDocComment + $useDocComment, )->getUsesTags(); foreach ($useTags as $useTag) { $useTagType = $useTag->getType(); @@ -516,85 +720,81 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia } if ($useType === null) { - return new NameScopedPhpDocString( - $original->getPhpDocString(), - $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()) - ); + return $original->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()); } $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); - return new NameScopedPhpDocString( - $original->getPhpDocString(), - $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->map(static function (string $name, Type $type) use ($transformedTraitTypeMap): Type { - return TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap); - })) - ); + return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic()))); }; } - $phpDocMap = array_merge($phpDocMap, $finalTraitPhpDocMap); + $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap); } } return null; }, - static function (\PhpParser\Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack): void { - 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); if (count($typeAliasStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($typeAliasStack); if (count($functionStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($functionStack); - } elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) { + } elseif ($node instanceof Node\Stmt\Namespace_) { $namespace = null; $uses = []; + $constUses = []; } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { if (count($functionStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($functionStack); + } 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 $phpDocMap; + return $nameScopeMap; } /** - * @param Doc|null $docComment * @return array */ - private function getTypeAliasesMap(?Doc $docComment): array + private function getTypeAliasesMap(PhpDocNode $phpDocNode): array { - if ($docComment === null) { - return []; - } - - $phpDocNode = $this->phpDocStringResolver->resolve($docComment->getText()); $nameScope = new NameScope(null, []); $aliasesMap = []; @@ -610,11 +810,11 @@ private function getTypeAliasesMap(?Doc $docComment): array } /** - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param \Closure(\PhpParser\Node $node): mixed $nodeCallback - * @param \Closure(\PhpParser\Node $node, mixed $callbackResult): void $endNodeCallback + * @param Node[]|Node|scalar|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); @@ -633,149 +833,22 @@ private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCa } } - private function getPhpDocKey( - string $file, + private function getNameScopeKey( + ?string $file, ?string $class, ?string $trait, ?string $function, - string $docComment ): string { - $cacheKey = md5($docComment); - if (!isset($this->docKeys[$cacheKey])) { - $this->docKeys[$cacheKey] = \Nette\Utils\Strings::replace($docComment, '#\s+#', ' '); - } - $docComment = $this->docKeys[$cacheKey]; - if ($class === null && $trait === null && $function === null) { - return md5(sprintf('%s-%s', $file, $docComment)); - } - - 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; - } + 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, - ]; + if ($class !== null && str_contains($class, 'class@anonymous')) { + throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().'); } - $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; - } - - $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; - } - } - } - - return null; - }, - static function (): void { - } - ); - - unset($this->alreadyProcessedDependentFiles[$fileName]); - - return $dependentFiles; + return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function)); } } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index d966b978fd..92ebb92cf1 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -2,68 +2,90 @@ 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 { - if ($type instanceof self || $type instanceof IntegerType) { - return TrinaryLogic::createYes(); + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof self || $type->isInteger()->yes()) { + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy(new UnionType([ - $this, - new IntegerType(), - ]), $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -81,6 +103,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toFloat(): Type { return $this; @@ -95,6 +122,7 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -104,31 +132,81 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isConstantValue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + public function isConstantScalarValue(): TrinaryLogic { - return new ErrorType(); + return TrinaryLogic::createNo(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + public function getConstantScalarTypes(): array { - return new ErrorType(); + return []; + } + + public function getConstantScalarValues(): array + { + return []; } - public function isArray(): TrinaryLogic + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -143,23 +221,79 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - * @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 ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('float'); + } + + public function getFiniteTypes(): array { - return new self(); + return []; } } diff --git a/src/Type/FunctionParameterClosureThisExtension.php b/src/Type/FunctionParameterClosureThisExtension.php new file mode 100644 index 0000000000..92d64d3a96 --- /dev/null +++ b/src/Type/FunctionParameterClosureThisExtension.php @@ -0,0 +1,33 @@ +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]; } @@ -36,9 +34,20 @@ public static function moreSpecific(): self return self::create(self::MORE_SPECIFIC); } + /** @api */ + public static function templateArgument(): self + { + return self::create(self::TEMPLATE_ARGUMENT); + } + 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 bcb5e67e56..4e04a9df28 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -2,13 +2,18 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; 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,18 +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; - /** @api */ - public function __construct(Type $type) + public function __construct(private Type $type) { parent::__construct(); - $this->type = $type; } public function getReferencedClasses(): array @@ -41,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) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getValue())) { - return TrinaryLogic::createNo(); + if (!$type->isClassString()->yes()) { + return AcceptsResult::createNo(); } $objectType = new ObjectType($type->getValue()); @@ -64,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); @@ -81,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) { @@ -100,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 @@ -124,6 +137,16 @@ public function traverse(callable $cb): Type return new self($newType); } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $newType = $cb($this->type, $right->getClassStringObjectType()); + if ($newType === $this->type) { + return $this; + } + + return new self($newType); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -134,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(); @@ -172,13 +195,31 @@ public function equals(Type $type): bool return true; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self($properties['type']); + return new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [ + $this->type->toPhpDocNode(), + ], + ); + } + + public function tryRemove(Type $typeToRemove): ?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 e0230b9fa9..54bef86b29 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,46 +2,50 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; -use PHPStan\TrinaryLogic; +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; /** @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 @@ -49,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, + )), ); } @@ -74,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(); @@ -100,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; @@ -138,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(); @@ -163,17 +178,30 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar continue; } if (!$templateType instanceof TemplateType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + $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[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); + $results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); } if (count($results) === 0) { return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(...$results); + $result = IsSuperTypeOfResult::createYes(); + foreach ($results as $innerResult) { + $result = $result->and($innerResult); + } + + return $result; } public function getClassReflection(): ?ClassReflection @@ -187,10 +215,12 @@ public function getClassReflection(): ?ClassReflection return null; } - return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName()) + ->withTypes($this->types) + ->withVariances($this->variances); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -202,7 +232,31 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $prototype->doNotResolveTemplateTypeMapToBounds(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $prototype = parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope); + + return $prototype->doNotResolveTemplateTypeMapToBounds(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $prototype = parent::getUnresolvedStaticPropertyPrototype($propertyName, $scope); + + return $prototype->doNotResolveTemplateTypeMapToBounds(); + } + + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -257,11 +311,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; } @@ -287,42 +342,75 @@ public function traverse(callable $cb): Type } if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { - return $this->recreate($this->getClassName(), $types, $subtractedType); + return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return $this->recreate($this->getClassName(), $types, null); } return $this; } /** - * @param string $className * @param Type[] $types - * @param Type|null $subtractedType - * @return self + * @param TemplateTypeVariance[] $variances */ - protected function recreate(string $className, array $types, ?Type $subtractedType): self + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self { return new self( $className, $types, - $subtractedType + $subtractedType, + null, + $variances, ); } public function changeSubtractedType(?Type $subtractedType): Type { - return new self($this->getClassName(), $this->types, $subtractedType); + return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self( - $properties['className'], - $properties['types'], - $properties['subtractedType'] ?? null + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), ); } diff --git a/src/Type/Generic/GenericStaticType.php b/src/Type/Generic/GenericStaticType.php new file mode 100644 index 0000000000..f677142e20 --- /dev/null +++ b/src/Type/Generic/GenericStaticType.php @@ -0,0 +1,273 @@ + $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 (array_keys($templateTags) as $typeName) { + if (!array_key_exists($i, $this->types)) { + break; + } + if (!array_key_exists($i, $this->variances)) { + break; + } + $indexedTypes[$typeName] = $this->types[$i]; + $indexedVariances[$typeName] = $this->variances[$i]; + $i++; + } + + $newType = new GenericObjectType($classReflection->getName(), $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $ancestorType = $newType->getAncestorWithClassName($this->getClassName()); + if ($ancestorType === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $ancestorClassReflection = $ancestorType->getClassReflection(); + if ($ancestorClassReflection === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $newClassTypes = []; + $newClassVariances = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + if (!array_key_exists($typeName, $indexedTypes)) { + continue; + } + + $newClassTypes[$templateType->getName()] = $indexedTypes[$typeName]; + $newClassVariances[$templateType->getName()] = $indexedVariances[$typeName]; + } + + return new self($classReflection, $classReflection->typeMapToList(new TemplateTypeMap($newClassTypes)), $this->subtractedType, $classReflection->varianceMapToList(new TemplateTypeVarianceMap($newClassVariances))); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type->getStaticObjectType()); + } + + return parent::isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); + } + + public function traverse(callable $cb): Type + { + $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; + + $typesChanged = false; + $types = []; + foreach ($this->types as $type) { + $newType = $cb($type); + $types[] = $newType; + if ($newType === $type) { + continue; + } + + $typesChanged = true; + } + + if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { + return new self( + $this->classReflection, + $types, + $subtractedType, + $this->variances, + ); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return new self( + $this->classReflection, + $types, + null, + $this->variances, + ); + } + + return $this; + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + if ($subtractedType !== null) { + $classReflection = $this->getClassReflection(); + if ($classReflection->getAllowedSubTypes() !== null) { + $objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType); + if ($objectType instanceof NeverType) { + return $objectType; + } + + if ($objectType instanceof ObjectType && $objectType->getSubtractedType() !== null) { + return new self($classReflection, $this->types, $objectType->getSubtractedType(), $this->variances); + } + + return TypeCombinator::intersect($this, $objectType); + } + } + + return new self( + $this->classReflection, + $this->types, + $subtractedType, + $this->variances, + ); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + return $this->getStaticObjectType()->inferTemplateTypes($receivedType); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->getStaticObjectType()->getReferencedTemplateTypes($positionVariance); + } + + public function toPhpDocNode(): TypeNode + { + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), + ); + } + +} diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php index ce577adb31..3899bba47a 100644 --- a/src/Type/Generic/TemplateArrayType.php +++ b/src/Type/Generic/TemplateArrayType.php @@ -14,12 +14,16 @@ final class TemplateArrayType extends ArrayType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ArrayType $bound + ArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyType(), $bound->getItemType()); @@ -28,27 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof ArrayType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 94e867a15b..aea8573131 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -12,12 +12,16 @@ final class TemplateBenevolentUnionType extends BenevolentUnionType implements T /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - BenevolentUnionType $bound + BenevolentUnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -27,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } /** @param Type[] $types */ @@ -37,24 +42,26 @@ public function withTypes(array $types): self $this->strategy, $this->variance, $this->name, - new BenevolentUnionType($types) + new BenevolentUnionType($types), + $this->default, ); } - public function traverse(callable $cb): Type + public function filterTypes(callable $filterCb): Type { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof BenevolentUnionType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); } - return $this; + return $result; } } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 76cd73ec70..f5fbddf7aa 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -14,12 +14,16 @@ final class TemplateBooleanType extends BooleanType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - BooleanType $bound + BooleanType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,27 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof BooleanType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index 2684484d6e..fbc13bad6d 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -14,41 +14,25 @@ final class TemplateConstantArrayType extends ConstantArrayType implements Templ use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ConstantArrayType $bound + ConstantArrayType $bound, + ?Type $default, ) { - parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndex(), $bound->getOptionalKeys()); + parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof ConstantArrayType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php new file mode 100644 index 0000000000..242ba6794f --- /dev/null +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -0,0 +1,38 @@ + */ + 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; + } + +} diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php new file mode 100644 index 0000000000..53179bd1f2 --- /dev/null +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -0,0 +1,38 @@ + */ + 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; + } + +} diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index 29171e1958..6a0d93b33d 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -14,12 +14,16 @@ final class TemplateFloatType extends FloatType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - FloatType $bound + FloatType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,27 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof FloatType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 2f5cd5616c..8236a7094e 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -13,47 +13,37 @@ final class TemplateGenericObjectType extends GenericObjectType implements Templ /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - GenericObjectType $bound + GenericObjectType $bound, + ?Type $default, ) { - parent::__construct($bound->getClassName(), $bound->getTypes()); + parent::__construct($bound->getClassName(), $bound->getTypes(), variances: $bound->getVariances()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof GenericObjectType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function recreate(string $className, array $types, ?Type $subtractedType): GenericObjectType + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType { return new self( $this->scope, $this->strategy, $this->variance, $this->name, - $this->getBound() + $this->getBound(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 552d407a89..2f3499630f 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -14,12 +14,16 @@ final class TemplateIntegerType extends IntegerType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - IntegerType $bound + IntegerType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,27 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof IntegerType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } 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/TemplateIterableType.php b/src/Type/Generic/TemplateIterableType.php new file mode 100644 index 0000000000..5308ad3b58 --- /dev/null +++ b/src/Type/Generic/TemplateIterableType.php @@ -0,0 +1,38 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + IterableType $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; + } + +} diff --git a/src/Type/Generic/TemplateKeyOfType.php b/src/Type/Generic/TemplateKeyOfType.php new file mode 100644 index 0000000000..dc28eaec13 --- /dev/null +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -0,0 +1,52 @@ + */ + 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(), + ); + } + +} diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index d661e2e77e..8160633fc3 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -14,12 +15,16 @@ final class TemplateMixedType extends MixedType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - MixedType $bound + MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -29,36 +34,21 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { return $this->isSuperTypeOf($type); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $isSuperType = $this->isSuperTypeOf($acceptingType); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); if ($isSuperType->no()) { return $isSuperType; } - return TrinaryLogic::createYes(); - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof MixedType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; + return AcceptsResult::createYes(); } public function toStrictMixedType(): TemplateStrictMixedType @@ -68,7 +58,8 @@ public function toStrictMixedType(): TemplateStrictMixedType $this->strategy, $this->variance, $this->name, - new StrictMixedType() + new StrictMixedType(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateNullType.php b/src/Type/Generic/TemplateNullType.php new file mode 100644 index 0000000000..7a5b95e598 --- /dev/null +++ b/src/Type/Generic/TemplateNullType.php @@ -0,0 +1,37 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + NullType $bound, + ?Type $default, + ) + { + parent::__construct(); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php new file mode 100644 index 0000000000..658cb7378d --- /dev/null +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -0,0 +1,38 @@ + */ + 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; + } + +} diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index 8617a42858..220414ca14 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -14,12 +14,16 @@ final class TemplateObjectType extends ObjectType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ObjectType $bound + ObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName()); @@ -29,22 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof ObjectType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 144a564e7c..7d6aebc6f9 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -14,12 +14,16 @@ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements T /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ObjectWithoutClassType $bound + ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -29,22 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof ObjectWithoutClassType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 2159e506bb..6feefd8696 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -14,12 +15,16 @@ final class TemplateStrictMixedType extends StrictMixedType implements TemplateT /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - StrictMixedType $bound + StrictMixedType $bound, + ?Type $default, ) { $this->scope = $scope; @@ -27,32 +32,17 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { return $this->isSuperTypeOf($type); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof StrictMixedType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } } diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 37a0e9b4e7..a9d8240be5 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -14,12 +14,16 @@ final class TemplateStringType extends StringType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - StringType $bound + StringType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,27 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof StringType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound - ); - } - - return $this; - } - - protected function shouldGeneralizeInferredType(): bool - { - return false; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index fb1e5039bd..2dfa5dafbd 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -2,26 +2,31 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; /** @api */ interface TemplateType extends CompoundType { + /** @return non-empty-string */ public function getName(): string; public function getScope(): TemplateTypeScope; public function getBound(): Type; + public function getDefault(): ?Type; + public function toArgument(): TemplateType; public function isArgument(): bool; - public function isValidVariance(Type $a, Type $b): TrinaryLogic; + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult; public function getVariance(): TemplateTypeVariance; + public function getStrategy(): TemplateTypeStrategy; + } diff --git a/src/Type/Generic/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index 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 252e434c76..fb3b2149bd 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -7,83 +7,122 @@ 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\IterableType; +use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; +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): TemplateType + /** + * @param non-empty-string $name + */ + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null, ?Type $default = null): TemplateType { - $strategy = new TemplateTypeParameterStrategy(); + $strategy ??= new TemplateTypeParameterStrategy(); if ($bound === null) { - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } $boundClass = get_class($bound); if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) { - return new TemplateObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) { - return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) { - return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { - return new TemplateArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantArrayType && ($boundClass === ConstantArrayType::class || $bound instanceof TemplateType)) { - return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ObjectShapeType && ($boundClass === ObjectShapeType::class || $bound instanceof TemplateType)) { + return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { - return new TemplateStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateStringType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ConstantStringType && ($boundClass === ConstantStringType::class || $bound instanceof TemplateType)) { + return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) { - return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ConstantIntegerType && ($boundClass === ConstantIntegerType::class || $bound instanceof TemplateType)) { + return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { - return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); + return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { - return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound); + return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { - return new TemplateMixedType($scope, $strategy, $variance, $name, $bound); + return new TemplateMixedType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof UnionType) { if ($boundClass === UnionType::class || $bound instanceof TemplateUnionType) { - return new TemplateUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateUnionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BenevolentUnionType) { - return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound, $default); } } - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + if ($bound instanceof IntersectionType) { + return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof KeyOfType && ($boundClass === KeyOfType::class || $bound instanceof TemplateType)) { + return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof IterableType && ($boundClass === IterableType::class || $bound instanceof TemplateType)) { + return new TemplateIterableType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof NullType && ($boundClass === NullType::class || $bound instanceof TemplateType)) { + return new TemplateNullType($scope, $strategy, $variance, $name, $bound, $default); + } + + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): TemplateType { - return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance()); + return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance(), default: $tag->getDefault()); } } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index b8d15d47e1..29ecd48720 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,33 +2,65 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\ConstantType; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\StringType; +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()); + + $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) { + if ($newType instanceof ErrorType && !$keepErrorTypes) { + return $traverse($type->getDefault() ?? $type->getBound()); + } + + $callSiteVariance = $callSiteVariances->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { return $traverse($type->getBound()); } + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + return $newType; } @@ -36,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 { @@ -54,8 +97,35 @@ public static function resolveToBounds(Type $type): Type */ public static function toArgument(Type $type): Type { + $ownedTemplates = []; + /** @var T */ - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type { + if ($type instanceof ParametersAcceptor) { + $templateTypeMap = $type->getTemplateTypeMap(); + + foreach ($type->getParameters() as $parameter) { + $parameterType = $parameter->getType(); + if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) { + continue; + } + + $ownedTemplates[] = $parameterType; + } + + $returnType = $type->getReturnType(); + + if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) { + $ownedTemplates[] = $returnType; + } + } + + foreach ($ownedTemplates as $ownedTemplate) { + if ($ownedTemplate === $type) { + return $traverse($type); + } + } + if ($type instanceof TemplateType) { return $traverse($type->toArgument()); } @@ -64,23 +134,18 @@ public static function toArgument(Type $type): Type }); } - public static function generalizeType(Type $type): Type + public static function generalizeInferredTemplateType(TemplateType $templateType, Type $type): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof ConstantType && !$type instanceof ConstantArrayType) { - return $type->generalize(GeneralizePrecision::lessSpecific()); - } - - if ($type->isNonEmptyString()->yes()) { - return new StringType(); - } - - if ($type->isLiteralString()->yes()) { - return new StringType(); + 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 $traverse($type); - }); + return $type; } } diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index ec864bb171..49f3a08496 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -2,33 +2,31 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function count; -/** @api */ -class TemplateTypeMap +/** + * @api + */ +final class TemplateTypeMap { private static ?TemplateTypeMap $empty = null; - /** @var array */ - private array $types; - - /** @var array */ - private array $lowerBoundTypes; + private ?TemplateTypeMap $resolvedToBounds = null; /** * @api - * @param array $types - * @param array $lowerBoundTypes + * @param array $types + * @param array $lowerBoundTypes */ - public function __construct(array $types, array $lowerBoundTypes = []) + public function __construct(private array $types, private array $lowerBoundTypes = []) { - $this->types = $types; - $this->lowerBoundTypes = $lowerBoundTypes; } public function convertToLowerBoundTypes(): self @@ -73,7 +71,7 @@ public function count(): int return count($this->types + $this->lowerBoundTypes); } - /** @return array */ + /** @return array */ public function getTypes(): array { $types = $this->types; @@ -210,25 +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'], - $properties['lowerBoundTypes'] ?? [] - ); + 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 5d9a8256d6..3e18bccf2d 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -2,17 +2,17 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Type; /** * Template type strategy suitable for parameter type acceptance contexts */ -class TemplateTypeParameterStrategy implements TemplateTypeStrategy +final class TemplateTypeParameterStrategy implements TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult { if ($right instanceof CompoundType) { return $right->isAcceptedBy($left, $strictTypes); @@ -26,12 +26,4 @@ public function isArgument(): bool return false; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self(); - } - } diff --git a/src/Type/Generic/TemplateTypeReference.php b/src/Type/Generic/TemplateTypeReference.php index 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 8d33fcc943..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,10 +27,8 @@ public static function createWithClass(string $className): self return new self($className, null); } - private function __construct(?string $className, ?string $functionName) + private function __construct(private ?string $className, private ?string $functionName) { - $this->className = $className; - $this->functionName = $functionName; } /** @api */ @@ -52,6 +53,10 @@ public function equals(self $other): bool /** @api */ public function describe(): string { + if ($this->className === null && $this->functionName === null) { + return 'anonymous function'; + } + if ($this->className === null) { return sprintf('function %s()', $this->functionName); } @@ -63,15 +68,4 @@ public function describe(): string return sprintf('method %s::%s()', $this->className, $this->functionName); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['className'], - $properties['functionName'] - ); - } - } diff --git a/src/Type/Generic/TemplateTypeStrategy.php b/src/Type/Generic/TemplateTypeStrategy.php index d90dc732e1..843710ae7c 100644 --- a/src/Type/Generic/TemplateTypeStrategy.php +++ b/src/Type/Generic/TemplateTypeStrategy.php @@ -2,13 +2,13 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\Type; interface TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic; + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult; public function isArgument(): bool; diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index d62acb63a5..584439c12d 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -2,13 +2,23 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\RecursionGuard; +use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @template TBound of Type @@ -16,6 +26,7 @@ trait TemplateTypeTrait { + /** @var non-empty-string */ private string $name; private TemplateTypeScope $scope; @@ -27,6 +38,9 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + private ?Type $default; + + /** @return non-empty-string */ public function getName(): string { return $this->name; @@ -43,27 +57,39 @@ 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 { - if ($this->bound instanceof MixedType) { // @phpstan-ignore-line + // @phpstan-ignore booleanAnd.alwaysFalse, instanceof.alwaysFalse, booleanAnd.alwaysFalse, instanceof.alwaysFalse, instanceof.alwaysTrue + if ($this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType) { $boundDescription = ''; - } else { // @phpstan-ignore-line + } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } + $defaultDescription = ''; + if ($this->default !== null) { + $recursionGuard = RecursionGuard::runOnObjectIdentity($this->default, fn () => $this->default->describe($level)); + if (!$recursionGuard instanceof ErrorType) { + $defaultDescription .= sprintf(' = %s', $recursionGuard); + } + } return sprintf( - '%s%s', + '%s%s%s', $this->name, - $boundDescription + $boundDescription, + $defaultDescription, ); }; return $level->handle( $basicDescription, $basicDescription, - function () use ($basicDescription): string { - return sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'); - } + fn (): string => sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'), ); } @@ -79,28 +105,71 @@ public function toArgument(): TemplateType new TemplateTypeArgumentStrategy(), $this->variance, $this->name, - TemplateTypeHelper::toArgument($this->getBound()) + TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } - public function isValidVariance(Type $a, Type $b): TrinaryLogic + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult { - return $this->variance->isValidVariance($a, $b); + return $this->variance->isValidVariance($this, $a, $b); } - public function subtract(Type $type): Type + public function subtract(Type $typeToRemove): Type { - return $this; + $removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove); + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $removedBound, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); } public function getTypeWithoutSubtractedType(): Type { - return $this; + $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 { - return $this; + $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 @@ -108,32 +177,60 @@ public function equals(Type $type): bool return $type instanceof self && $type->scope->equals($this->scope) && $type->name === $this->name - && $this->bound->equals($type->bound); + && $this->bound->equals($type->bound) + && ( + ($this->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + /** @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): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return $this->strategy->accepts($this, $type, $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof CompoundType) { + if ($type instanceof TemplateType || $type instanceof IntersectionType) { return $type->isSubTypeOf($this); } + if ($type instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + return $this->getBound()->isSuperTypeOf($type) - ->and(TrinaryLogic::createMaybe()); + ->and(IsSuperTypeOfResult::createMaybe()); } - public function isSubTypeOf(Type $type): TrinaryLogic + public function isSubTypeOf(Type $type): IsSuperTypeOfResult { - /** @var Type $bound */ + /** @var TBound $bound */ $bound = $this->getBound(); if ( !$type instanceof $bound @@ -148,24 +245,26 @@ public function isSubTypeOf(Type $type): TrinaryLogic return $type->isSuperTypeOf($this->getBound()); } - if ($this->equals($type)) { - return TrinaryLogic::createYes(); + if ($this->getScope()->equals($type->getScope()) && $this->getName() === $type->getName()) { + return $type->getBound()->isSuperTypeOf($this->getBound()); } - if ($type->getBound()->isSuperTypeOf($this->getBound())->no() && - $this->getBound()->isSuperTypeOf($type->getBound())->no()) { - return TrinaryLogic::createNo(); - } + return $type->getBound()->isSuperTypeOf($this->getBound()) + ->and(IsSuperTypeOfResult::createMaybe()); + } - return TrinaryLogic::createMaybe(); + public function toArrayKey(): Type + { + return $this; } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function toCoercedArgumentType(bool $strictTypes): Type { - if (!$receivedType instanceof TemplateType && ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType)) { - return $receivedType->inferTemplateTypesOn($this); - } + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { if ( $receivedType instanceof TemplateType && $this->getBound()->isSuperTypeOf($receivedType->getBound())->yes() @@ -176,10 +275,15 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } $map = $this->getBound()->inferTemplateTypes($receivedType); - $resolvedBound = TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map); + $resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes( + $this->getBound(), + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + )); if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { return (new TemplateTypeMap([ - $this->name => $this->shouldGeneralizeInferredType() ? TemplateTypeHelper::generalizeType($receivedType) : $receivedType, + $this->name => $receivedType, ]))->union($map); } @@ -196,24 +300,73 @@ public function getVariance(): TemplateTypeVariance return $this->variance; } - protected function shouldGeneralizeInferredType(): bool + public function getStrategy(): TemplateTypeStrategy { - return true; + return $this->strategy; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function traverse(callable $cb): Type { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'], - $properties['bound'] + $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 3a0aa9217f..a630895bed 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,33 +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; -/** @api */ -class TemplateTypeVariance +/** + * @api + */ +final class TemplateTypeVariance { private const INVARIANT = 1; private const COVARIANT = 2; private const CONTRAVARIANT = 3; private const STATIC = 4; + private const BIVARIANT = 5; /** @var self[] */ private static array $registry; - 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]; } @@ -52,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; @@ -72,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()) { @@ -81,46 +96,79 @@ public function compose(self $other): self if ($other->covariant()) { return self::createContravariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } if ($this->covariant()) { if ($other->contravariant()) { - return self::createCovariant(); + return self::createContravariant(); } if ($other->covariant()) { return self::createCovariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } + return self::createInvariant(); + } + + if ($this->invariant()) { return self::createInvariant(); } + if ($this->bivariant()) { + return self::createBivariant(); + } + return $other; } - public function isValidVariance(Type $a, Type $b): TrinaryLogic + public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult { + if ($b instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + if ($a instanceof MixedType && !$a instanceof TemplateType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($a instanceof BenevolentUnionType) { if (!$a->isSuperTypeOf($b)->no()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof BenevolentUnionType) { if (!$b->isSuperTypeOf($a)->no()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof MixedType && !$b instanceof TemplateType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($this->invariant()) { - return TrinaryLogic::createFromBoolean($a->equals($b)); + $result = $a->equals($b); + $reasons = []; + if (!$result) { + if ( + $templateType->getScope()->getClassName() !== null + && $a->isSuperTypeOf($b)->yes() + ) { + $reasons[] = sprintf( + 'Template type %s on class %s is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + $templateType->getName(), + $templateType->getScope()->getClassName(), + ); + } + } + + return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons); } if ($this->covariant()) { @@ -131,7 +179,11 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic return $b->isSuperTypeOf($a); } - throw new \PHPStan\ShouldNotHappenException(); + if ($this->bivariant()) { + return IsSuperTypeOfResult::createYes(); + } + + throw new ShouldNotHappenException(); } public function equals(self $other): bool @@ -143,6 +195,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -157,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 index 2dea6a3c1e..dc58af565a 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -12,12 +12,16 @@ final class TemplateUnionType extends UnionType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - UnionType $bound + UnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -27,22 +31,24 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function traverse(callable $cb): Type + public function filterTypes(callable $filterCb): Type { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof UnionType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); } - return $this; + return $result; } } diff --git a/src/Type/Generic/TypeProjectionHelper.php b/src/Type/Generic/TypeProjectionHelper.php new file mode 100644 index 0000000000..c311cde2f8 --- /dev/null +++ b/src/Type/Generic/TypeProjectionHelper.php @@ -0,0 +1,31 @@ +describe($level); + + if ($variance === null || $variance->invariant()) { + return $describedType; + } + + if ($variance->bivariant()) { + return '*'; + } + + return sprintf('%s %s', $variance->describe(), $describedType); + } + +} diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php deleted file mode 100644 index 5d6abefe43..0000000000 --- a/src/Type/GenericTypeVariableResolver.php +++ /dev/null @@ -1,30 +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 ab643d34da..50eac67a04 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -2,27 +2,44 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use function array_filter; +use function array_map; +use function assert; +use function ceil; +use function count; +use function floor; +use function get_class; +use function is_float; +use function is_int; +use function max; +use function min; +use function sprintf; +use const PHP_INT_MAX; +use const PHP_INT_MIN; /** @api */ class IntegerRangeType extends IntegerType implements CompoundType { - private ?int $min; - - private ?int $max; - - public function __construct(?int $min, ?int $max) + private function __construct(private ?int $min, private ?int $max) { - // this constructor can be made private when PHP 7.2 is the minimum parent::__construct(); assert($min === null || $max === null || $min <= $max); assert($min !== null || $max !== null); - - $this->min = $min; - $this->max = $max; } public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type @@ -54,7 +71,6 @@ protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $m * Return the range of integers smaller than the given value * * @param int|float $value - * @return Type */ public static function createAllSmallerThan($value): Type { @@ -77,7 +93,6 @@ public static function createAllSmallerThan($value): Type * Return the range of integers smaller than or equal to the given value * * @param int|float $value - * @return Type */ public static function createAllSmallerThanOrEqualTo($value): Type { @@ -100,7 +115,6 @@ public static function createAllSmallerThanOrEqualTo($value): Type * Return the range of integers greater than the given value * * @param int|float $value - * @return Type */ public static function createAllGreaterThan($value): Type { @@ -123,7 +137,6 @@ public static function createAllGreaterThan($value): Type * Return the range of integers greater than or equal to the given value * * @param int|float $value - * @return Type */ public static function createAllGreaterThanOrEqualTo($value): Type { @@ -147,19 +160,16 @@ public function getMin(): ?int return $this->min; } - public function getMax(): ?int { return $this->max; } - public function describe(VerbosityLevel $level): string { return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max'); } - public function shift(int $amount): Type { if ($amount === 0) { @@ -194,22 +204,20 @@ public function shift(int $amount): Type return self::fromInterval($min, $max); } - - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof parent) { - return $this->isSuperTypeOf($type); + return $this->isSuperTypeOf($type)->toAcceptsResult(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self || $type instanceof ConstantIntegerType) { if ($type instanceof self) { @@ -221,46 +229,66 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ( ($this->min === null || $typeMin !== null && $this->min <= $typeMin) && ($this->max === null || $typeMax !== null && $this->max >= $typeMax) ) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof parent) { return $otherType->isSuperTypeOf($this); } - if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + if ($otherType instanceof UnionType) { + return $this->isSubTypeOfUnionWithReason($otherType); + } + + if ($otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + private function isSubTypeOfUnionWithReason(UnionType $otherType): IsSuperTypeOfResult { - return $this->isSubTypeOf($acceptingType); + if ($this->min !== null && $this->max !== null) { + $matchingConstantIntegers = array_filter( + $otherType->getTypes(), + fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max, + ); + + if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) { + return IsSuperTypeOfResult::createYes(); + } + } + + return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType) => $this->isSubTypeOf($innerType), $otherType->getTypes())); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -268,81 +296,120 @@ public function equals(Type $type): bool return $type instanceof self && $this->min === $type->min && $this->max === $type->max; } - public function generalize(GeneralizePrecision $precision): Type { - return new parent(); + return new IntegerType(); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createYes(); } else { - $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType); + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType, $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createNo(); } else { - $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType); + $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThan($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createYes(); } else { - $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType); + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType, $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createNo(); } else { - $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType); + $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThanOrEqual($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createNo(); } else { - $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min))); + $minIsSmaller = $otherType->isSmallerThan(new ConstantIntegerType($this->min), $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createYes(); } else { - $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max))); + $maxIsSmaller = $otherType->isSmallerThan(new ConstantIntegerType($this->max), $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThan($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createNo(); } else { - $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min))); + $minIsSmaller = $otherType->isSmallerThanOrEqual(new ConstantIntegerType($this->min), $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createYes(); } else { - $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max))); + $maxIsSmaller = $otherType->isSmallerThanOrEqual(new ConstantIntegerType($this->max), $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThanOrEqual($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -355,7 +422,7 @@ public function getSmallerType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = []; @@ -366,7 +433,7 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new NullType(), @@ -384,7 +451,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = []; @@ -414,11 +481,50 @@ public function toBoolean(): BooleanType return new ConstantBooleanType(false); } + public function toAbsoluteNumber(): Type + { + if ($this->min !== null && $this->min >= 0) { + return $this; + } + + if ($this->max === null || $this->max >= 0) { + $inversedMin = $this->min !== null ? $this->min * -1 : null; + + return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + } + + return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null); + } + + public function toString(): Type + { + $finiteTypes = $this->getFiniteTypes(); + if ($finiteTypes !== []) { + return TypeCombinator::union(...$finiteTypes)->toString(); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); + if ($isZero->no()) { + return new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + return new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + ]); + } + /** * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType * - * @param Type $otherType - * @return Type|null */ public function tryUnion(Type $otherType): ?Type { @@ -437,7 +543,7 @@ public function tryUnion(Type $otherType): ?Type return self::fromInterval( $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null, - $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null + $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null, ); } @@ -452,8 +558,6 @@ public function tryUnion(Type $otherType): ?Type * Return the intersection with another type, but only if it can be expressed in a simpler way than using * IntersectionType * - * @param Type $otherType - * @return Type|null */ public function tryIntersect(Type $otherType): ?Type { @@ -499,8 +603,6 @@ public function tryIntersect(Type $otherType): ?Type /** * Return the different with another type, or null if it cannot be represented. * - * @param Type $typeToRemove - * @return Type|null */ public function tryRemove(Type $typeToRemove): ?Type { @@ -508,8 +610,8 @@ public function tryRemove(Type $typeToRemove): ?Type return new NeverType(); } - if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { - if ($typeToRemove instanceof IntegerRangeType) { + if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) { + if ($typeToRemove instanceof self) { $removeMin = $typeToRemove->min; $removeMax = $typeToRemove->max; } else { @@ -545,13 +647,111 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + if ($exponent instanceof UnionType) { + $results = []; + foreach ($exponent->getTypes() as $unionType) { + $results[] = $this->exponentiate($unionType); + } + return TypeCombinator::union(...$results); + } + + if ($exponent instanceof IntegerRangeType) { + $min = null; + $max = null; + if ($this->getMin() !== null && $exponent->getMin() !== null) { + $min = $this->getMin() ** $exponent->getMin(); + } + if ($this->getMax() !== null && $exponent->getMax() !== null) { + $max = $this->getMax() ** $exponent->getMax(); + } + + if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + + if ($exponent instanceof ConstantScalarType) { + $exponentValue = $exponent->getValue(); + if (is_int($exponentValue)) { + $min = null; + $max = null; + if ($this->getMin() !== null) { + $min = $this->getMin() ** $exponentValue; + } + if ($this->getMax() !== null) { + $max = $this->getMax() ** $exponentValue; + } + + if (!is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + } + + return parent::exponentiate($exponent); + } + /** - * @param mixed[] $properties - * @return Type + * @return list */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + if ($this->min === null || $this->max === null) { + return []; + } + + $size = $this->max - $this->min; + if ($size > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + $types = []; + for ($i = $this->min; $i <= $this->max; $i++) { + $types[] = new ConstantIntegerType($i); + } + + return $types; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['min'], $properties['max']); + 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 b562754258..0dd713bee8 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,14 +2,23 @@ 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; @@ -18,12 +27,15 @@ 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() @@ -35,13 +47,9 @@ 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 @@ -49,6 +57,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + public function toFloat(): Type { return new FloatType(); @@ -63,6 +76,8 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -72,28 +87,116 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isFalse(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isArray()->yes()) { + return new ConstantBooleanType(false); + } + + if ( + $phpVersion->nonNumericStringAndIntegerIsFalseOnLooseComparison() + && $type->isString()->yes() + && $type->isNumericString()->no() + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { + 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, bool $unionValues = true): 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 8003be5471..07f2aca42c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,38 +2,86 @@ 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\MissingConstantFromReflectionException; +use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\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_filter; +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)), + )); + } } /** @@ -45,69 +93,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 { - if ($otherType instanceof IntersectionType && $this->equals($otherType)) { - return TrinaryLogic::createYes(); + $reflections = []; + foreach ($this->types as $type) { + foreach ($type->getObjectClassReflections() as $reflection) { + $reflections[] = $reflection; + } } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + return $reflections; + } + + public function getArrays(): array + { + $arrays = []; + foreach ($this->types as $type) { + foreach ($type->getArrays() as $array) { + $arrays[] = $array; + } } - return TrinaryLogic::createYes()->and(...$results); + return $arrays; } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function getConstantArrays(): array { - if ($otherType instanceof self || $otherType instanceof UnionType) { - return $otherType->isSuperTypeOf($this); + $constantArrays = []; + foreach ($this->types as $type) { + foreach ($type->getConstantArrays() as $constantArray) { + $constantArrays[] = $constantArray; + } + } + + return $constantArrays; + } + + public function getConstantStrings(): array + { + $strings = []; + foreach ($this->types as $type) { + foreach ($type->getConstantStrings() as $string) { + $strings[] = $string; + } + } + + return $strings; + } + + public function accepts(Type $otherType, bool $strictTypes): AcceptsResult + { + $result = AcceptsResult::createYes(); + foreach ($this->types as $type) { + $result = $result->and($type->accepts($otherType, $strictTypes)); } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $otherType->isSuperTypeOf($innerType); + if (!$result->yes()) { + $isList = $otherType->isList(); + $reasons = $result->reasons; + $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType); + if ($this->isList()->yes() && !$isList->yes()) { + $reasons[] = sprintf( + '%s %s a list.', + $otherType->describe($verbosity), + $isList->no() ? 'is not' : 'might not be', + ); + } + + $isNonEmpty = $otherType->isIterableAtLeastOnce(); + if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) { + $reasons[] = sprintf( + '%s %s empty.', + $otherType->describe($verbosity), + $isNonEmpty->no() ? 'is' : 'might be', + ); + } + + if (count($reasons) > 0) { + return new AcceptsResult($result->result, $reasons); + } } - return TrinaryLogic::maxMin(...$results); + return $result; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { - if ($acceptingType instanceof self || $acceptingType instanceof UnionType) { - return $acceptingType->isSuperTypeOf($this); + if ($otherType instanceof IntersectionType && $this->equals($otherType)) { + return IsSuperTypeOfResult::createYes(); } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); + if ($otherType instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::maxMin(...$results); + 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; } @@ -115,13 +278,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 @@ -129,72 +304,213 @@ 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, GeneralizePrecision::lessSpecific())->describe($level); + $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level); } - return implode('&', $typeNames); - }, - function () use ($level): string { - $typeNames = []; - $accessoryTypes = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType) { - $accessoryTypes[] = $type; - } - if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType && !$type instanceof NonEmptyArrayType && !$type instanceof AccessoryNonEmptyStringType) { - 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); - } - if (count($accessoryTypes) > 0) { - return implode('&', array_map(static function (Type $type) use ($level): string { - return $type->describe($level); - }, $accessoryTypes)); + $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 = []; - $accessoryTypes = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType) { - $accessoryTypes[] = $type; + 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() + && !$level->isCache() + ) { + 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]); } - $typeNames[] = $type->describe($level); } - if (count($accessoryTypes) > 0) { - return implode('&', array_map(static function (Type $type) use ($level): string { - return $type->describe($level); - }, $accessoryTypes)); + $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; + } + } - return implode('&', $typeNames); + 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(); } @@ -212,7 +528,73 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $propertiesCount = count($propertyPrototypes); if ($propertiesCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName)); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName)); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasStaticProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); } if ($propertiesCount === 1) { @@ -224,19 +606,15 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember public function canCallMethods(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -254,7 +632,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName); } if ($methodsCount === 1) { @@ -266,19 +644,15 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce public function canAccessConstants(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName)); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { foreach ($this->types as $type) { if ($type->hasConstant($constantName)->yes()) { @@ -286,108 +660,370 @@ public function getConstant(string $constantName): ConstantReflection } } - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName); } 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(), + static fn (Type $type): bool => !$type->isIterable()->no(), + ); + } + + 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 function (Type $type): TrinaryLogic { - return $type->isNumericString(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } public function isNonEmptyString(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isNonEmptyString(); - }); + if ($this->isCallable()->yes() && $this->isString()->yes()) { + return TrinaryLogic::createYes(); + } + 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 function (Type $type): TrinaryLogic { - return $type->isLiteralString(); - }); + 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 { - return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { - return $type->setOffsetValueType($offsetType, $valueType, $unionValues); - }); + if ($this->isOversizedArray()->yes()) { + return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { + // avoid new HasOffsetValueType being intersected with oversized array + if (!$type instanceof ArrayType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + return TypeCombinator::intersect( + new ArrayType( + TypeCombinator::union($type->getKeyType(), $offsetType), + TypeCombinator::union($type->getItemType(), $valueType), + ), + new NonEmptyArrayType(), + ); + }); + } + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ( + $offsetType !== null + && $this->isList()->yes() + && !$result->isList()->yes() + ) { + if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } else { + foreach ($this->types as $type) { + if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) { + continue; + } + + foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + continue; + } + if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + break 2; + } + } + } + } + } + + if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } + + 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict)); + } + + public function getKeysArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray()); + } + + public function getValuesArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys)); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + + public function flipArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + } + + public function popArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); + } + + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); + } + + 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()]; @@ -395,72 +1031,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 isSmallerThan(Type $otherType): TrinaryLogic + public function isNull(): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThan($otherType); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull()); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isConstantValue(): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThanOrEqual($otherType); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isConstantScalarValue(): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThan($type); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function getConstantScalarTypes(): array { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThanOrEqual($type); - }); + $scalarTypes = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarTypes() as $scalarType) { + $scalarTypes[] = $scalarType; + } + } + + return $scalarTypes; } - public function getSmallerType(): Type + public function getConstantScalarValues(): array { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getSmallerType(); - }); + $values = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarValues() as $value) { + $values[] = $value; + } + } + + return $values; } - public function getSmallerOrEqualType(): Type + public function isTrue(): TrinaryLogic { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getSmallerOrEqualType(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); } - public function getGreaterType(): Type + public function isFalse(): TrinaryLogic { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getGreaterType(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); } - public function getGreaterOrEqualType(): Type + public function isBoolean(): TrinaryLogic { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getGreaterOrEqualType(); - }); + 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(); @@ -471,66 +1151,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; @@ -569,28 +1256,87 @@ 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 + * @param (callable(Type $type): bool)|null $filter */ - private function intersectResults(callable $getResult): TrinaryLogic + private function intersectResults( + callable $getResult, + ?callable $filter = null, + ): TrinaryLogic { - $operands = array_map($getResult, $this->types); - return TrinaryLogic::maxMin(...$operands); + $types = $this->types; + if ($filter !== null) { + $types = array_filter($types, $filter); + } + if (count($types) === 0) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::lazyMaxMin($types, $getResult); } /** * @param callable(Type $type): Type $getType - * @return Type */ private function intersectTypes(callable $getType): Type { @@ -598,4 +1344,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 46c21dad4b..c2b44b809b 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -2,40 +2,45 @@ 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 get_class; +use function sprintf; /** @api */ class IterableType implements CompoundType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; use UndecidedBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private \PHPStan\Type\Type $keyType; - - private \PHPStan\Type\Type $itemType; + use NonGeneralizableTypeTrait; /** @api */ public function __construct( - Type $keyType, - Type $itemType + private Type $keyType, + private Type $itemType, ) { - $this->keyType = $keyType; - $this->itemType = $itemType; } public function getKeyType(): Type @@ -48,21 +53,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 + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof ConstantArrayType && $type->isEmpty()) { - return TrinaryLogic::createYes(); + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return AcceptsResult::createYes(); } if ($type->isIterable()->yes()) { return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes) @@ -73,24 +90,28 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return $type->isIterable() + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return (new IsSuperTypeOfResult($type->isIterable(), [])) ->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType())) ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } - public function isSuperTypeOfMixed(Type $type): TrinaryLogic + public function isSuperTypeOfMixed(Type $type): IsSuperTypeOfResult { - return $type->isIterable() + return (new IsSuperTypeOfResult($type->isIterable(), [])) ->and($this->isNestedTypeSuperTypeOf($this->getIterableValueType(), $type->getIterableValueType())) ->and($this->isNestedTypeSuperTypeOf($this->getIterableKeyType(), $type->getIterableKeyType())); } - private function isNestedTypeSuperTypeOf(Type $a, Type $b): TrinaryLogic + private function isNestedTypeSuperTypeOf(Type $a, Type $b): IsSuperTypeOfResult { if (!$a instanceof MixedType || !$b instanceof MixedType) { return $a->isSuperTypeOf($b); @@ -102,52 +123,52 @@ 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 { - if (!$type instanceof self) { + if (get_class($type) !== static::class) { return false; } @@ -157,9 +178,8 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - $isMixedKeyType = $this->keyType instanceof MixedType && !$this->keyType instanceof TemplateType; - $isMixedItemType = $this->itemType instanceof MixedType && !$this->itemType instanceof TemplateType; - + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; if ($isMixedKeyType) { if ($isMixedItemType) { return 'iterable'; @@ -185,6 +205,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -205,6 +230,32 @@ 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 isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -215,19 +266,94 @@ 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 @@ -240,11 +366,61 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -263,9 +439,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), ); } @@ -281,13 +459,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 703c5b4c66..5435c540ff 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -3,43 +3,50 @@ 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 $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 @@ -52,7 +59,62 @@ 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(); } @@ -67,9 +129,44 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Type/KeyOfType.php b/src/Type/KeyOfType.php new file mode 100644 index 0000000000..10a4cb2ea5 --- /dev/null +++ b/src/Type/KeyOfType.php @@ -0,0 +1,94 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('key-of<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getIterableKeyType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('key-of'), [$this->type->toPhpDocNode()]); + } + +} diff --git a/src/Type/LateResolvableType.php b/src/Type/LateResolvableType.php new file mode 100644 index 0000000000..5c95444ce9 --- /dev/null +++ b/src/Type/LateResolvableType.php @@ -0,0 +1,13 @@ +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/MethodParameterClosureThisExtension.php b/src/Type/MethodParameterClosureThisExtension.php new file mode 100644 index 0000000000..663a0d801e --- /dev/null +++ b/src/Type/MethodParameterClosureThisExtension.php @@ -0,0 +1,33 @@ +isExplicitMixed = $isExplicitMixed; $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 isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); + } + + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { if ($this->subtractedType === null) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->subtractedType === null || $type instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof self) { if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { - return TrinaryLogic::createYes(); + return $isSuperType; } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return $this->subtractedType->isSuperTypeOf($type)->negate(); + $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 MixedType(); + 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + public function getKeysArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()])), new AccessoryArrayListType()); + } + + public function getValuesArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function fillKeysArray(Type $valueType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType($this->getIterableValueType(), $valueType); + } + + public function flipArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function popArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::union(new IntegerType(), new StringType(), new ConstantBooleanType(false)); + } + + public function shiftArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function shuffleArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): 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()]; @@ -144,7 +327,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function equals(Type $type): bool { - if (!$type instanceof self) { + if (get_class($type) !== static::class) { return false; } @@ -163,29 +346,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 @@ -198,21 +406,61 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, + ); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, ); } @@ -226,7 +474,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -238,9 +486,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -254,9 +500,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 @@ -267,25 +513,11 @@ 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', + fn (): string => 'mixed' . $this->describeSubtractedType($this->subtractedType, $level), function () use ($level): string { - $description = 'mixed'; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - }, - function () use ($level): string { - $description = 'mixed'; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); - } + $description = 'mixed' . $this->describeSubtractedType($this->subtractedType, $level); if ($this->isExplicitMixed) { $description .= '=explicit'; @@ -294,14 +526,16 @@ function () use ($level): string { } return $description; - } + }, ); } public function toBoolean(): BooleanType { - if ($this->subtractedType !== null && StaticTypeFactory::falsey()->equals($this->subtractedType)) { - return new ConstantBooleanType(true); + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(StaticTypeFactory::falsey())->yes()) { + return new ConstantBooleanType(true); + } } return new BooleanType(); @@ -309,14 +543,37 @@ public function toBoolean(): BooleanType public function toNumber(): Type { - return new UnionType([ + return TypeCombinator::union( $this->toInteger(), $this->toFloat(), - ]); + ); + } + + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); } public function toInteger(): Type { + $castsToZero = new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + new StringType(), + new FloatType(), // every 0.x float casts to int(0) + ]); + if ( + $this->subtractedType !== null + && $this->subtractedType->isSuperTypeOf($castsToZero)->yes() + ) { + return new UnionType([ + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]); + } + return new IntegerType(); } @@ -327,12 +584,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 @@ -372,36 +762,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(); + } + + public function isConstantArray(): TrinaryLogic + { + return $this->isArray(); + } + + public function isOversizedArray(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $oversizedArray = TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new OversizedArrayType(), + ); + + if ($this->subtractedType->isSuperTypeOf($oversizedArray)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $list = TypeCombinator::intersect( + new ArrayType(new IntegerType(), new MixedType()), + new AccessoryArrayListType(), + ); + + if ($this->subtractedType->isSuperTypeOf($list)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new NullType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(true))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFalse(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(false))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isBoolean(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new BooleanType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFloat(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new FloatType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isInteger(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new IntegerType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { + return TrinaryLogic::createNo(); + } + } return TrinaryLogic::createMaybe(); } public function isNumericString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($numericString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isNonEmptyString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $nonEmptyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonEmptyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $nonFalsyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonFalsyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isLiteralString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $literalString = TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($literalString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic { - return new self( - $properties['isExplicitMixed'], - $properties['subtractedType'] ?? null - ); + 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 049544332d..5fc9a05280 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -2,32 +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; - - private bool $isExplicit; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(bool $isExplicit = false) + public function __construct(private bool $isExplicit = false) { - $this->isExplicit = $isExplicit; } public function isExplicit(): bool @@ -35,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 isSuperTypeOf(Type $type): TrinaryLogic + 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): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -62,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 @@ -77,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(); @@ -87,14 +128,44 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -107,14 +178,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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -127,9 +198,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 @@ -142,21 +213,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(); @@ -168,22 +289,98 @@ public function getOffsetValueType(Type $offsetType): 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + + 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, ?TrinaryLogic $strict = null): 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): 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 @@ -196,6 +393,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toString(): Type { return $this; @@ -216,12 +418,77 @@ 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(); } @@ -236,18 +503,74 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return $this; + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['isExplicit']); + return new IdentifierTypeNode('never'); } } diff --git a/src/Type/NewObjectType.php b/src/Type/NewObjectType.php new file mode 100644 index 0000000000..93d14c6936 --- /dev/null +++ b/src/Type/NewObjectType.php @@ -0,0 +1,94 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('new<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getObjectTypeOrClassStringObjectType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]); + } + +} diff --git a/src/Type/NonAcceptingNeverType.php b/src/Type/NonAcceptingNeverType.php new file mode 100644 index 0000000000..dd14d3f9d2 --- /dev/null +++ b/src/Type/NonAcceptingNeverType.php @@ -0,0 +1,41 @@ +isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -81,27 +100,35 @@ public function equals(Type $type): bool return $type instanceof self; } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean(null < $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThan($this); + return $otherType->isGreaterThan($this, $phpVersion); + } + + if ($otherType->isObject()->yes()) { + return TrinaryLogic::createYes(); } return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean(null <= $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThanOrEqual($this); + return $otherType->isGreaterThanOrEqual($this, $phpVersion); + } + + if ($otherType->isObject()->yes()) { + return TrinaryLogic::createYes(); } return TrinaryLogic::createMaybe(); @@ -117,6 +144,11 @@ public function toNumber(): Type return new ConstantIntegerType(0); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType(''); @@ -137,11 +169,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(); @@ -158,12 +205,77 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $array->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -178,17 +290,75 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getSmallerType(): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.alwaysTrue, equal.notAllowed + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + if ($type instanceof CompoundType) { + return $type->looseCompare($this, $phpVersion); + } + + return new BooleanType(); + } + + public function getSmallerType(PhpVersion $phpVersion): Type { return new NeverType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { // All falsey types except '0' return new UnionType([ @@ -201,10 +371,10 @@ public function getSmallerOrEqualType(): Type ]); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { // All truthy types, but also '0' - return new MixedType(false, new UnionType([ + return new MixedType(subtractedType: new UnionType([ new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), @@ -214,18 +384,29 @@ public function getGreaterType(): Type ])); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + return [$this]; + } + + public function exponentiate(Type $exponent): Type + { + return new UnionType( + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], + ); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('null'); } } diff --git a/src/Type/ObjectShapePropertyReflection.php b/src/Type/ObjectShapePropertyReflection.php new file mode 100644 index 0000000000..bc727ac4fc --- /dev/null +++ b/src/Type/ObjectShapePropertyReflection.php @@ -0,0 +1,162 @@ +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 isFinalByKeyword(): 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 []; + } + + public function isDummy(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php new file mode 100644 index 0000000000..361190316b --- /dev/null +++ b/src/Type/ObjectShapeType.php @@ -0,0 +1,558 @@ + $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 + { + return $this->hasInstanceProperty($propertyName); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasInstanceProperty(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 getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(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 hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + 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->hasInstanceProperty((string) $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->yes() && $type->hasStaticProperty((string) $propertyName)->yes()) { + $result = $result->and(new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is static.', $type->getStaticProperty((string) $propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), + ])); + continue; + } + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + $result = $result->and($hasProperty); + continue; + } + if ($hasProperty->maybe()) { + if (!in_array($propertyName, $this->optionalProperties, true)) { + $result = $result->and($hasProperty); + continue; + + } + + $hasProperty = AcceptsResult::createYes(); + } + + $result = $result->and($hasProperty); + try { + $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + continue; + } + + 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->hasInstanceProperty((string) $propertyName), []); + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + $result = $result->and($hasProperty); + continue; + } + if ($hasProperty->maybe()) { + if (!in_array($propertyName, $this->optionalProperties, true)) { + $result = $result->and($hasProperty); + continue; + } + + $hasProperty = IsSuperTypeOfResult::createYes(); + } + + $result = $result->and($hasProperty); + try { + $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + continue; + } + + 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 (int|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 (int|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->hasInstanceProperty((string) $name)->no()) { + continue; + } + + try { + $receivedProperty = $receivedType->getInstanceProperty((string) $name, $scope); + } catch (MissingPropertyFromReflectionException) { + continue; + } + if (!$receivedProperty->isPublic()) { + continue; + } + if ($receivedProperty->isStatic()) { + continue; + } + $receivedPropertyType = $receivedProperty->getReadableType(); + $typeMap = $typeMap->union($propertyType->inferTemplateTypes($receivedPropertyType)); + } + + return $typeMap; + } + + return TemplateTypeMap::createEmpty(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + $references = []; + foreach ($this->properties as $propertyType) { + foreach ($propertyType->getReferencedTemplateTypes($variance) as $reference) { + $references[] = $reference; + } + } + + return $references; + } + + public function describe(VerbosityLevel $level): string + { + $callback = function () use ($level): string { + $items = []; + foreach ($this->properties as $name => $propertyType) { + $optional = in_array($name, $this->optionalProperties, true); + $items[] = sprintf('%s%s: %s', $name, $optional ? '?' : '', $propertyType->describe($level)); + } + return sprintf('object{%s}', implode(', ', $items)); + }; + return $level->handle( + $callback, + $callback, + ); + } + + public function getEnumCases(): array + { + return []; + } + + public function traverse(callable $cb): Type + { + $properties = []; + $stillOriginal = true; + + foreach ($this->properties as $name => $propertyType) { + $transformed = $cb($propertyType); + if ($transformed !== $propertyType) { + $stillOriginal = false; + } + + $properties[$name] = $transformed; + } + + if ($stillOriginal) { + return $this; + } + + return new self($properties, $this->optionalProperties); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right->isObject()->yes()) { + return $this; + } + + $properties = []; + $stillOriginal = true; + + $scope = new OutOfClassScope(); + foreach ($this->properties as $name => $propertyType) { + if (!$right->hasInstanceProperty((string) $name)->yes()) { + return $this; + } + $transformed = $cb($propertyType, $right->getInstanceProperty((string) $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((string) $name)) { + $keyNode = new IdentifierTypeNode((string) $name); + } else { + $keyPhpDocNode = (new ConstantStringType((string) $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 adc64f9d43..de1e120173 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -2,45 +2,92 @@ namespace PHPStan\Type; +use ArrayAccess; +use ArrayObject; +use Closure; +use Countable; +use DateTimeInterface; +use Iterator; +use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; -use PHPStan\Broker\Broker; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\FunctionCallableVariant; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Dummy\DummyPropertyReflection; +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\CallbackUnresolvedPropertyPrototypeReflection; 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\Accessory\HasOffsetValueType; 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\SubstractableTypeTrait; 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 MaybeIterableTypeTrait; + use NonArrayTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; - - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; - - private string $className; - - private ?\PHPStan\Type\Type $subtractedType; - - private ?ClassReflection $classReflection; - - /** @var array> */ + use NonGeneralizableTypeTrait; + use SubstractableTypeTrait; + + 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 ?self $cachedParent = null; @@ -54,26 +101,35 @@ class ObjectType implements TypeWithClassName, SubtractableType /** @var array>> */ private static array $properties = []; - /** @var array> */ + /** @var array>> */ + private static array $instanceProperties = []; + + /** @var array>> */ + private static array $staticProperties = []; + + /** @var array> */ private static array $ancestors = []; - /** @var array */ + /** @var array */ private array $currentAncestors = []; + private ?string $cachedDescription = null; + + /** @var array> */ + private static array $enumCases = []; + /** @api */ public function __construct( - string $className, + private string $className, ?Type $subtractedType = null, - ?ClassReflection $classReflection = null + private ?ClassReflection $classReflection = null, ) { if ($subtractedType instanceof NeverType) { $subtractedType = null; } - $this->className = $className; $this->subtractedType = $subtractedType; - $this->classReflection = $classReflection; } public static function resetCaches(): void @@ -81,19 +137,10 @@ public static function resetCaches(): void self::$superTypes = []; self::$methods = []; self::$properties = []; + self::$instanceProperties = []; + self::$staticProperties = []; self::$ancestors = []; - } - - private static function createFromReflection(ClassReflection $reflection): self - { - if (!$reflection->isGeneric()) { - return new ObjectType($reflection->getName()); - } - - return new GenericObjectType( - $reflection->getName(), - $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()) - ); + self::$enumCases = []; } public function getClassName(): string @@ -108,18 +155,23 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->hasProperty($propertyName)) { + $classHasProperty = RecursionGuard::run($this, static fn (): bool => $classReflection->hasProperty($propertyName)); + if ($classHasProperty === true || $classHasProperty instanceof ErrorType) { return TrinaryLogic::createYes(); } - if ($classReflection->isFinal()) { - return TrinaryLogic::createNo(); + if ($classReflection->allowsDynamicProperties()) { + return TrinaryLogic::createMaybe(); } - return TrinaryLogic::createMaybe(); + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -139,22 +191,52 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $nakedClassReflection = $this->getNakedClassReflection(); if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); + } + + if ($nakedClassReflection->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->hasProperty($propertyName)) { + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { $nakedClassReflection = $this->getClassReflection(); } if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } - $property = $nakedClassReflection->getProperty($propertyName, $scope); + $property = RecursionGuard::run($this, static fn () => $nakedClassReflection->getProperty($propertyName, $scope)); + if ($property instanceof ErrorType) { + $property = new DummyPropertyReflection($propertyName); + + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; - if ($ancestor !== null) { + if ($ancestor !== null && $ancestor->hasProperty($propertyName)->yes()) { $resolvedClassReflection = $ancestor->getClassReflection(); if ($ancestor !== $this) { $property = $ancestor->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty(); @@ -168,37 +250,222 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $property, $resolvedClassReflection, true, - $this + $this, ); } - public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function hasInstanceProperty(string $propertyName): TrinaryLogic { - $classReflection = $this->getNakedClassReflection(); + $classReflection = $this->getClassReflection(); if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + return TrinaryLogic::createMaybe(); } - if (!$classReflection->hasProperty($propertyName)) { - $classReflection = $this->getClassReflection(); + $classHasProperty = RecursionGuard::run($this, static fn (): bool => $classReflection->hasInstanceProperty($propertyName)); + if ($classHasProperty === true || $classHasProperty instanceof ErrorType) { + return TrinaryLogic::createYes(); + } + + if ($classReflection->allowsDynamicProperties()) { + return TrinaryLogic::createMaybe(); + } + + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!$scope->isInClass()) { + $canAccessProperty = 'no'; + } else { + $canAccessProperty = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + + if (isset(self::$instanceProperties[$description][$propertyName][$canAccessProperty])) { + return self::$instanceProperties[$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->getUnresolvedInstancePropertyPrototype($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 = RecursionGuard::run($this, static fn () => $nakedClassReflection->getInstanceProperty($propertyName, $scope)); + if ($property instanceof ErrorType) { + $property = new DummyPropertyReflection($propertyName); + + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null && $ancestor->hasInstanceProperty($propertyName)->yes()) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $property = $ancestor->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getNakedProperty(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $property->getDeclaringClass(); + } + + return self::$instanceProperties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $property, + $resolvedClassReflection, + true, + $this, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + $classReflection = $this->getClassReflection(); if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + return TrinaryLogic::createMaybe(); + } + + $classHasProperty = RecursionGuard::run($this, static fn (): bool => $classReflection->hasStaticProperty($propertyName)); + if ($classHasProperty === true || $classHasProperty instanceof ErrorType) { + return TrinaryLogic::createYes(); + } + + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); } - return $classReflection->getProperty($propertyName, $scope); + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!$scope->isInClass()) { + $canAccessProperty = 'no'; + } else { + $canAccessProperty = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + + if (isset(self::$staticProperties[$description][$propertyName][$canAccessProperty])) { + return self::$staticProperties[$description][$propertyName][$canAccessProperty]; + } + + $nakedClassReflection = $this->getNakedClassReflection(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { + $nakedClassReflection = $this->getClassReflection(); + } + + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + $property = RecursionGuard::run($this, static fn () => $nakedClassReflection->getStaticProperty($propertyName)); + if ($property instanceof ErrorType) { + $property = new DummyPropertyReflection($propertyName); + + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null && $ancestor->hasStaticProperty($propertyName)->yes()) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $property = $ancestor->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getNakedProperty(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $property->getDeclaringClass(); + } + + return self::$staticProperties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $property, + $resolvedClassReflection, + true, + $this, + ); } - /** - * @return string[] - */ public function getReferencedClasses(): array { return [$this->className]; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + if ($this->className === '') { + return []; + } + return [$this->className]; + } + + public function getObjectClassReflections(): array + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return []; + } + + return [$classReflection]; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof StaticType) { return $this->checkSubclassAcceptability($type->getClassName()); @@ -209,22 +476,32 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof ClosureType) { - return $this->isInstanceOf(\Closure::class); + return new AcceptsResult($this->isInstanceOf(Closure::class), []); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } - if (!$type instanceof TypeWithClassName) { - return TrinaryLogic::createNo(); + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); } - return $this->checkSubclassAcceptability($type->getClassName()); + if ($thatClassNames === []) { + return AcceptsResult::createNo(); + } + + return $this->checkSubclassAcceptability($thatClassNames[0]); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + $thatClassNames = $type->getObjectClassNames(); + if (!$type instanceof CompoundType && $thatClassNames === [] && !$type instanceof ObjectWithoutClassType) { + return IsSuperTypeOfResult::createNo(); + } + $thisDescription = $this->describeCache(); if ($type instanceof self) { @@ -241,24 +518,28 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = $type->isSubTypeOf($this); } + if ($type instanceof ClosureType) { + return self::$superTypes[$thisDescription][$description] = new IsSuperTypeOfResult($this->isInstanceOf(Closure::class), []); + } + if ($type instanceof ObjectWithoutClassType) { if ($type->getSubtractedType() !== null) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); - } - - if (!$type instanceof TypeWithClassName) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } + $transformResult = static fn (IsSuperTypeOfResult $result) => $result; if ($this->subtractedType !== null) { $isSuperType = $this->subtractedType->isSuperTypeOf($type); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } + if ($isSuperType->maybe()) { + $transformResult = static fn (IsSuperTypeOfResult $result) => $result->and(IsSuperTypeOfResult::createMaybe()); } } @@ -268,47 +549,69 @@ public function isSuperTypeOf(Type $type): TrinaryLogic ) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } } $thisClassName = $this->className; - $thatClassName = $type->getClassName(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } + + $thisClassReflection = $this->getClassReflection(); + $thatClassReflections = $type->getObjectClassReflections(); + if (count($thatClassReflections) === 1) { + $thatClassReflection = $thatClassReflections[0]; + } else { + $thatClassReflection = null; + } - if ($thatClassName === $thisClassName) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + if ($thisClassReflection === null || $thatClassReflection === null) { + if ($thatClassNames[0] === $thisClassName) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - $reflectionProvider = ReflectionProviderStaticAccessor::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 || !$reflectionProvider->hasClass($thatClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - $thisClassReflection = $this->getClassReflection(); - $thatClassReflection = $reflectionProvider->getClass($thatClassName); + if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } if ($thisClassReflection->getName() === $thatClassReflection->getName()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thatClassReflection->isSubclassOf($thisClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + if ($thatClassReflection->isSubclassOfClass($thisClassReflection)) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thisClassReflection->isSubclassOf($thatClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isSubclassOfClass($thatClassReflection)) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isInterface() && !$thatClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thatClassReflection->isInterface() && !$thisClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -317,16 +620,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) { @@ -336,16 +639,16 @@ public function equals(Type $type): bool return $this->subtractedType->equals($type->subtractedType); } - private function checkSubclassAcceptability(string $thatClass): TrinaryLogic + private function checkSubclassAcceptability(string $thatClass): AcceptsResult { if ($this->className === $thatClass) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } $thisReflection = $this->getClassReflection(); @@ -353,17 +656,17 @@ private function checkSubclassAcceptability(string $thatClass): TrinaryLogic if ($thisReflection->getName() === $thatReflection->getName()) { // class alias - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($thisReflection->isInterface() && $thatReflection->isInterface()) { - return TrinaryLogic::createFromBoolean( - $thatReflection->implementsInterface($this->className) + return AcceptsResult::createFromBoolean( + $thatReflection->implementsInterface($thisReflection->getName()), ); } - return TrinaryLogic::createFromBoolean( - $thatReflection->isSubclassOf($this->className) + return AcceptsResult::createFromBoolean( + $thatReflection->isSubclassOfClass($thisReflection), ); } @@ -378,14 +681,7 @@ public function describe(VerbosityLevel $level): string return $reflectionProvider->getClassName($this->className); }; - $preciseWithSubtracted = function () use ($level): string { - $description = $this->className; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - }; + $preciseWithSubtracted = fn (): string => $this->className . $this->describeSubtractedType($this->subtractedType, $level); return $level->handle( $preciseNameCallback, @@ -401,7 +697,7 @@ function () use ($preciseWithSubtracted): string { } return $preciseWithSubtracted() . '-' . static::class . '-' . $line . $this->describeAdditionalCacheKey(); - } + }, ); } @@ -412,23 +708,39 @@ protected function describeAdditionalCacheKey(): string private function describeCache(): string { + if ($this->cachedDescription !== null) { + return $this->cachedDescription; + } + if (static::class !== self::class) { - return $this->describe(VerbosityLevel::cache()); + return $this->cachedDescription = $this->describe(VerbosityLevel::cache()); } $description = $this->className; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); + + if ($this instanceof GenericObjectType) { + $description .= '<'; + $typeDescriptions = []; + foreach ($this->getTypes() as $type) { + $typeDescriptions[] = $type->describe(VerbosityLevel::cache()); + } + $description .= '<' . implode(', ', $typeDescriptions) . '>'; } + $description .= $this->describeSubtractedType($this->subtractedType, 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 $description; + return $this->cachedDescription = $description; } public function toNumber(): Type @@ -443,6 +755,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { @@ -466,13 +783,21 @@ public function toFloat(): Type public function toString(): Type { + if ($this->isInstanceOf('BcMath\Number')->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonEmptyStringType(), + ]); + } + $classReflection = $this->getClassReflection(); if ($classReflection === null) { return new ErrorType(); } if ($classReflection->hasNativeMethod('__toString')) { - return ParametersAcceptorSelector::selectSingle($this->getMethod('__toString', new OutOfClassScope())->getVariants())->getReturnType(); + return $this->getMethod('__toString', new OutOfClassScope())->getOnlyVariant()->getReturnType(); } return new ErrorType(); @@ -489,10 +814,10 @@ public function toArray(): Type if ( !$classReflection->getNativeReflection()->isUserDefined() + || $classReflection->is(ArrayObject::class) || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $reflectionProvider, - Broker::getInstance()->getUniversalObjectCratesClasses(), - $classReflection + $classReflection, ) ) { return new ArrayType(new MixedType(), new MixedType()); @@ -500,6 +825,8 @@ public function toArray(): Type $arrayKeys = []; $arrayValues = []; + $isFinal = $classReflection->isFinal(); + do { foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) { if ($nativeProperty->isStatic()) { @@ -514,12 +841,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, ); } @@ -530,31 +857,100 @@ public function toArray(): Type $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); + if (!$isFinal) { + if (count($arrayKeys) === 0 || count($arrayKeys) > 16) { + return new ArrayType(new MixedType(), new MixedType()); + } + + $types = [new ArrayType(new MixedType(), new MixedType())]; + foreach ($arrayKeys as $i => $arrayKey) { + $types[] = new HasOffsetValueType($arrayKey, $arrayValues[$i]); + } + + return new IntersectionType($types); + } + return new ConstantArrayType($arrayKeys, $arrayValues); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + $classReflection = $this->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->hasNativeMethod('__toString') + ) { + return $this; + } + + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + public function toBoolean(): BooleanType { - if ($this->isInstanceOf('SimpleXMLElement')->yes()) { + if ( + $this->isInstanceOf('SimpleXMLElement')->yes() + || $this->isInstanceOf('BcMath\Number')->yes() + ) { return new BooleanType(); } return new ConstantBooleanType(true); } - public function canAccessProperties(): TrinaryLogic + public function isObject(): TrinaryLogic { return TrinaryLogic::createYes(); } - public function canCallMethods(): TrinaryLogic + public function isEnum(): TrinaryLogic { - if (strtolower($this->className) === 'stdclass') { - return TrinaryLogic::createNo(); + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); } - return TrinaryLogic::createYes(); - } + 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(); + } + + public function canCallMethods(): TrinaryLogic + { + if (strtolower($this->className) === 'stdclass') { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createYes(); + } public function hasMethod(string $methodName): TrinaryLogic { @@ -574,7 +970,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -593,15 +989,15 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $nakedClassReflection = $this->getNakedClassReflection(); if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } - if (!$nakedClassReflection->hasMethod($methodName)) { + if (!$nakedClassReflection->hasNativeMethod($methodName)) { $nakedClassReflection = $this->getClassReflection(); } if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } $method = $nakedClassReflection->getMethod($methodName, $scope); @@ -622,7 +1018,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $resolvedClassReflection, true, - $this + $this, ); } @@ -633,109 +1029,241 @@ public function canAccessConstants(): TrinaryLogic public function hasConstant(string $constantName): TrinaryLogic { - $class = $this->getClassReflection(); - if ($class === null) { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); + } + + if ($classReflection->hasConstant($constantName)) { + return TrinaryLogic::createYes(); + } + + if ($classReflection->isFinal()) { return TrinaryLogic::createNo(); } - return TrinaryLogic::createFromBoolean( - $class->hasConstant($constantName) - ); + return TrinaryLogic::createMaybe(); } - 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 getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return new ErrorType(); + } + + $ancestorClassReflection = $classReflection->getAncestorWithClassName($ancestorClassName); + if ($ancestorClassReflection === null) { + return new ErrorType(); + } + + $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); + $type = $activeTemplateTypeMap->getType($templateTypeName); + if ($type === null) { + return new ErrorType(); + } + if ($type instanceof ErrorType) { + $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); + $templateType = $templateTypeMap->getType($templateTypeName); + if ($templateType === null) { + return $type; + } + + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); + } + + return TemplateTypeHelper::resolveToDefaults($templateType); + } + + return $type; + } + + public function getConstantStrings(): array + { + return []; + } + public function isIterable(): TrinaryLogic { - return $this->isInstanceOf(\Traversable::class); + return $this->isInstanceOf(Traversable::class); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->isInstanceOf(\Traversable::class) + return $this->isInstanceOf(Traversable::class) ->and(TrinaryLogic::createMaybe()); } - public function getIterableKeyType(): Type + public function getArraySize(): Type { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { + if ($this->isInstanceOf(Countable::class)->no()) { return new ErrorType(); } - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('key', new OutOfClassScope())->getVariants() - )->getReturnType(); - }); + if ($this->hasMethod('count')->yes() === false) { + return IntegerRangeType::fromInterval(0, null); } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - $keyType = RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants() - )->getReturnType()->getIterableKeyType(); - }); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('count', new OutOfClassScope())->getOnlyVariant()->getReturnType()); + } + + 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()) { - $tKey = GenericTypeVariableResolver::getType($this, \Traversable::class, 'TKey'); - if ($tKey !== null) { - return $tKey; + $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 getIterableValueType(): Type + public function getFirstIterableKeyType(): Type { - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('current', new OutOfClassScope())->getVariants() - )->getReturnType(); - }); - } + return $this->getIterableKeyType(); + } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - $valueType = RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants() - )->getReturnType()->getIterableValueType(); - }); + 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; } } - 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; + $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 isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -750,11 +1278,62 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + return $type->isFalse()->yes() + ? new ConstantBooleanType(false) + : new BooleanType(); + } + private function isExtraOffsetAccessibleClass(): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -763,10 +1342,7 @@ private function isExtraOffsetAccessibleClass(): TrinaryLogic } foreach (self::EXTRA_OFFSET_CLASSES as $extraOffsetClass) { - if ($classReflection->getName() === $extraOffsetClass) { - return TrinaryLogic::createYes(); - } - if ($classReflection->isSubclassOf($extraOffsetClass)) { + if ($classReflection->is($extraOffsetClass)) { return TrinaryLogic::createYes(); } } @@ -784,21 +1360,26 @@ private function isExtraOffsetAccessibleClass(): TrinaryLogic public function isOffsetAccessible(): TrinaryLogic { - return $this->isInstanceOf(\ArrayAccess::class)->or( - $this->isExtraOffsetAccessibleClass() + return $this->isInstanceOf(ArrayAccess::class)->or( + $this->isExtraOffsetAccessibleClass(), ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->isOffsetAccessible(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedOffsetType = RecursionGuard::run($this, function (): Type { - $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); + $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', )); } @@ -820,14 +1401,12 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - if (!$this->isExtraOffsetAccessibleClass()->no()) { - return new MixedType(); + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { - return RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType(); - }); + if (!$this->isExtraOffsetAccessibleClass()->no()) { + return new MixedType(); } return new ErrorType(); @@ -839,15 +1418,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedValueType = new NeverType(); $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type { - $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); + $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', )); } @@ -873,12 +1452,75 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ($this->isOffsetAccessible()->no()) { + return new ErrorType(); + } + + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->isOffsetAccessible()->no()) { + 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 @@ -890,25 +1532,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 { @@ -918,10 +1556,14 @@ private function findCallableParametersAcceptors(): ?array } if ($classReflection->hasNativeMethod('__invoke')) { - return $this->getMethod('__invoke', new OutOfClassScope())->getVariants(); + $method = $this->getMethod('__invoke', new OutOfClassScope()); + return FunctionCallableVariant::createFromVariants( + $method, + $method->getVariants(), + ); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } @@ -933,18 +1575,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(); @@ -952,10 +1582,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(); } @@ -979,6 +1617,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); } @@ -994,26 +1681,34 @@ 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; } - $this->classReflection = $reflectionProvider->getClass($this->className); - - return $this->classReflection; + return $reflectionProvider->getClass($this->className); } public function getClassReflection(): ?ClassReflection @@ -1021,6 +1716,7 @@ public function getClassReflection(): ?ClassReflection if ($this->classReflection !== null) { return $this->classReflection; } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if (!$reflectionProvider->hasClass($this->className)) { return null; @@ -1028,38 +1724,44 @@ public function getClassReflection(): ?ClassReflection $classReflection = $reflectionProvider->getClass($this->className); if ($classReflection->isGeneric()) { - return $this->classReflection = $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->resolveToBounds()->getTypes())); + return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); } - return $this->classReflection = $classReflection; + return $classReflection; } - /** - * @param string $className - * @return self|null - */ - public function getAncestorWithClassName(string $className): ?TypeWithClassName + public function getAncestorWithClassName(string $className): ?self { - if (isset($this->currentAncestors[$className])) { - return $this->currentAncestors[$className]; + if ($this->className === $className) { + return $this; } - $thisReflection = $this->getClassReflection(); - if ($thisReflection === null) { - return null; + if ($this->classReflection !== null && $className === $this->classReflection->getName()) { + return $this; + } + + if (array_key_exists($className, $this->currentAncestors)) { + return $this->currentAncestors[$className]; } - $description = $this->describeCache() . '-' . $thisReflection->getCacheKey(); - if (isset(self::$ancestors[$description][$className])) { + $description = $this->describeCache(); + if ( + array_key_exists($description, self::$ancestors) + && array_key_exists($className, self::$ancestors[$description]) + ) { return self::$ancestors[$description][$className]; } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if (!$reflectionProvider->hasClass($className)) { - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } $theirReflection = $reflectionProvider->getClass($className); + $thisReflection = $this->getClassReflection(); + if ($thisReflection === null) { + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; + } if ($theirReflection->getName() === $thisReflection->getName()) { return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $this; } @@ -1079,7 +1781,7 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName } } - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } private function getParent(): ?ObjectType @@ -1097,7 +1799,7 @@ private function getParent(): ?ObjectType return null; } - return $this->cachedParent = self::createFromReflection($parentReflection); + return $this->cachedParent = $parentReflection->getObjectType(); } /** @return ObjectType[] */ @@ -1111,9 +1813,53 @@ private function getInterfaces(): array return $this->cachedInterfaces = []; } - return $this->cachedInterfaces = array_map(static function (ClassReflection $interfaceReflection): self { - return self::createFromReflection($interfaceReflection); - }, $thisReflection->getInterfaces()); + return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => $interfaceReflection->getObjectType(), $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(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->getClassName()); } } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 23984955a3..9823dcaf80 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; -use PHPStan\TrinaryLogic; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\SubstractableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; /** @api */ @@ -14,12 +17,14 @@ class ObjectWithoutClassType implements SubtractableType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; + use SubstractableTypeTrait; - private ?\PHPStan\Type\Type $subtractedType; + private ?Type $subtractedType; /** @api */ public function __construct( - ?Type $subtractedType = null + ?Type $subtractedType = null, ) { if ($subtractedType instanceof NeverType) { @@ -29,26 +34,33 @@ public function __construct( $this->subtractedType = $subtractedType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean( - $type instanceof self || $type instanceof TypeWithClassName + return AcceptsResult::createFromBoolean( + $type instanceof self || $type instanceof ObjectShapeType || $type->getObjectClassNames() !== [], ); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); @@ -56,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 @@ -103,23 +119,17 @@ 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'; - }, - function () use ($level): string { - $description = 'object'; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - } + static fn (): string => 'object', + static fn (): string => 'object', + fn (): string => 'object' . $this->describeSubtractedType($this->subtractedType, $level), ); } + public function getEnumCases(): array + { + return []; + } + public function subtract(Type $type): Type { if ($type instanceof self) { @@ -147,7 +157,6 @@ public function getSubtractedType(): ?Type return $this->subtractedType; } - public function traverse(callable $cb): Type { $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null; @@ -159,13 +168,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 dc26ce3633..a9d2fbe129 100644 --- a/src/Type/OperatorTypeSpecifyingExtension.php +++ b/src/Type/OperatorTypeSpecifyingExtension.php @@ -2,7 +2,25 @@ namespace PHPStan\Type; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * how arithmetic operators like +, -, *, ^, / should infer types + * for PHP extensions that overload the behaviour, like GMP. + * + * To register it in the configuration file use the `phpstan.broker.operatorTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.operatorTypeSpecifyingExtension + * ``` + * + * Learn more: https://github.com/phpstan/phpstan/pull/2114 + * + * @api + */ interface OperatorTypeSpecifyingExtension { diff --git a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php index a1624cbe5e..11f65849b7 100644 --- a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php +++ b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php @@ -2,31 +2,21 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; +use PhpParser\Node\Expr; +use function array_filter; +use function array_values; +use function count; -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 +24,26 @@ 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))); + } + + public function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type $leftType, Type $rightType): ?Type + { + $operatorSigil = $expr->getOperatorSigil(); + $operatorTypeSpecifyingExtensions = $this->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; } } diff --git a/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php b/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..c15f13bf24 --- /dev/null +++ b/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php @@ -0,0 +1,68 @@ +getName() === 'createIdentifier'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $secondPartType = $scope->getType($args[0]->value); + $secondPartValues = $secondPartType->getConstantStrings(); + if (count($secondPartValues) === 0) { + return null; + } + + $reflection = new ReflectionClass(ClassNameUsageLocation::class); + $identifiers = []; + $locationValueType = $scope->getType(new PropertyFetch($methodCall->var, 'value')); + foreach ($reflection->getConstants() as $constant) { + if (!$locationValueType->isSuperTypeOf($scope->getTypeFromValue($constant))->yes()) { + continue; + } + $location = ClassNameUsageLocation::from($constant); + foreach ($secondPartValues as $secondPart) { + $identifiers[] = $location->createIdentifier($secondPart->getValue()); + } + } + + sort($identifiers); + + $types = []; + foreach ($identifiers as $identifier) { + $types[] = $scope->getTypeFromValue($identifier); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index 4ed5f5b10a..e616fffc0e 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -2,19 +2,22 @@ namespace PHPStan\Type; +use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\NullableType; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; +use function get_class; +use function in_array; +use function strtolower; -class ParserNodeTypeToPHPStanType +final class ParserNodeTypeToPHPStanType { /** - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\ComplexType|null $type - * @param ClassReflection|null $classReflection - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public static function resolve($type, ?ClassReflection $classReflection): Type { @@ -39,22 +42,27 @@ public static function resolve($type, ?ClassReflection $classReflection): Type return new ObjectType($typeClassName); } elseif ($type instanceof NullableType) { return TypeCombinator::addNull(self::resolve($type->type, $classReflection)); - } elseif ($type instanceof \PhpParser\Node\UnionType) { + } elseif ($type instanceof Node\UnionType) { $types = []; foreach ($type->types as $unionTypeType) { $types[] = self::resolve($unionTypeType, $classReflection); } return TypeCombinator::union(...$types); - } elseif ($type instanceof \PhpParser\Node\IntersectionType) { + } elseif ($type instanceof Node\IntersectionType) { $types = []; foreach ($type->types as $intersectionTypeType) { - $types[] = self::resolve($intersectionTypeType, $classReflection); + $innerType = self::resolve($intersectionTypeType, $classReflection); + if (!$innerType->isObject()->yes()) { + return new NeverType(); + } + + $types[] = $innerType; } return TypeCombinator::intersect(...$types); } elseif (!$type instanceof Identifier) { - throw new \PHPStan\ShouldNotHappenException(get_class($type)); + throw new ShouldNotHappenException(get_class($type)); } $type = $type->name; @@ -76,12 +84,16 @@ public static function resolve($type, ?ClassReflection $classReflection): Type return new VoidType(); } elseif ($type === 'object') { return new ObjectWithoutClassType(); + } elseif ($type === 'true') { + return new ConstantBooleanType(true); } elseif ($type === 'false') { return new ConstantBooleanType(false); } elseif ($type === 'null') { 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..7de2eebbd7 --- /dev/null +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,45 @@ +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 b4e4942f8a..b039b62d00 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -4,20 +4,22 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var int[] */ - private array $functionNames = [ + private const FUNCTION_NAMES = [ 'array_unique' => 0, - 'array_change_key_case' => 0, 'array_diff_assoc' => 0, 'array_diff_key' => 0, 'array_diff_uassoc' => 0, @@ -27,27 +29,25 @@ 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->getArgs()[$argumentPosition])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argument = $functionCall->getArgs()[$argumentPosition]; @@ -55,14 +55,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argumentKeyType = $argumentType->getIterableKeyType(); $argumentValueType = $argumentType->getIterableValueType(); if ($argument->unpack) { - $argumentKeyType = TypeUtils::generalizeType($argumentKeyType, GeneralizePrecision::moreSpecific()); - $argumentValueType = TypeUtils::generalizeType($argumentValueType->getIterableValueType(), GeneralizePrecision::moreSpecific()); + $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..d722c87283 --- /dev/null +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -0,0 +1,161 @@ +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..1028ae2fa1 --- /dev/null +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -0,0 +1,53 @@ +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..4fec6cfe3e --- /dev/null +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -0,0 +1,51 @@ +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) : new NullType(); + + $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..179d85bc04 --- /dev/null +++ b/src/Type/Php/ArrayColumnHelper.php @@ -0,0 +1,189 @@ +isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return [new NeverType(), $iterableAtLeastOnce]; + } + + $iterableValueType = $arrayType->getIterableValueType(); + [$returnValueType, $certainty] = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope); + + if (!$certainty->yes()) { + $iterableAtLeastOnce = TrinaryLogic::createMaybe(); + } + + return [$returnValueType, $iterableAtLeastOnce]; + } + + public function getReturnIndexType(Type $arrayType, Type $indexType, Scope $scope): Type + { + if (!$indexType->isNull()->yes()) { + $iterableValueType = $arrayType->getIterableValueType(); + + [$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope); + if ($certainty->yes()) { + return $type; + } + + 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->isNull()->yes()) { + $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, $certainty] = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope); + if (!$certainty->yes()) { + return null; + } + if ($valueType instanceof NeverType) { + continue; + } + + if (!$indexType->isNull()->yes()) { + [$type, $certainty] = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope); + if ($certainty->yes()) { + $keyType = $type; + } else { + $keyType = TypeCombinator::union($type, new IntegerType()); + } + } else { + $keyType = null; + } + + if ($keyType !== null) { + $keyType = $this->castToArrayKeyType($keyType); + } + $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); + } + + return $builder->getArray(); + } + + /** + * @return array{Type, TrinaryLogic} + */ + private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope): array + { + $offsetIsNull = $offsetOrProperty->isNull(); + if ($offsetIsNull->yes()) { + return [$type, TrinaryLogic::createYes()]; + } + + $returnTypes = []; + + if ($offsetIsNull->maybe()) { + $returnTypes[] = $type; + } + + if (!$type->canAccessProperties()->no()) { + $propertyTypes = $offsetOrProperty->getConstantStrings(); + if ($propertyTypes === []) { + return [new MixedType(), TrinaryLogic::createMaybe()]; + } + foreach ($propertyTypes as $propertyType) { + $propertyName = $propertyType->getValue(); + $hasProperty = $type->hasInstanceProperty($propertyName); + if ($hasProperty->maybe()) { + return [new MixedType(), TrinaryLogic::createMaybe()]; + } + if (!$hasProperty->yes()) { + continue; + } + + $returnTypes[] = $type->getInstanceProperty($propertyName, $scope)->getReadableType(); + } + } + + $certainty = TrinaryLogic::createYes(); + if ($type->isOffsetAccessible()->yes()) { + $hasOffset = $type->hasOffsetValueType($offsetOrProperty); + if ($hasOffset->maybe()) { + $certainty = TrinaryLogic::createMaybe(); + } + if (!$hasOffset->no()) { + $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); + } + } + + if ($returnTypes === []) { + return [new NeverType(), TrinaryLogic::createYes()]; + } + + return [TypeCombinator::union(...$returnTypes), $certainty]; + } + + 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 index 2a6a6854f3..663859a069 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -5,28 +5,31 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; -class ArrayCombineFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -34,10 +37,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_combine'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $firstArg = $functionCall->getArgs()[0]->value; @@ -54,21 +57,43 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $valueTypes = $valuesParamType->getValueTypes(); if (count($keyTypes) !== count($valueTypes)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } return new ConstantBooleanType(false); } $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); if ($keyTypes !== null) { - return new ConstantArrayType( - $keyTypes, - $valueTypes - ); + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($keyTypes as $i => $keyType) { + $valueType = $valueTypes[$i]; + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); } } + if ($keysParamType->isArray()->yes()) { + $itemType = $keysParamType->getIterableValueType(); + + if ($itemType->isInteger()->no()) { + if ($itemType->toString() instanceof ErrorType) { + return new NeverType(); + } + + $keyType = $itemType->toString(); + } else { + $keyType = $itemType; + } + } else { + $keyType = new MixedType(); + } + $arrayType = new ArrayType( - $keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(), - $valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType() + $keyType, + $valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(), ); if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { @@ -96,6 +121,10 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array $sanitizedTypes = []; foreach ($types as $type) { + if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) { + $type = $type->toString(); + } + if ( !$type instanceof ConstantIntegerType && !$type instanceof ConstantStringType diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php index dab9cdf5c8..2514aad8f8 100644 --- a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayCurrentDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -18,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'current'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 6faf9b4279..63cf6d9cf4 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -4,31 +4,31 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -36,33 +36,26 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_fill'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 3) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); $numberType = $scope->getType($functionCall->getArgs()[1]->value); - $valueType = $scope->getType($functionCall->getArgs()[2]->value); - - if ($numberType instanceof IntegerRangeType) { - if ($numberType->getMin() < 0) { - return TypeCombinator::union( - new ArrayType(new IntegerType(), $valueType), - new ConstantBooleanType(false) - ); - } - } + $isValidNumberType = IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($numberType); // check against negative-int, which is not allowed - if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($numberType)->yes()) { + if ($isValidNumberType->no()) { if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); } + $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); + $valueType = $scope->getType($functionCall->getArgs()[2]->value); + if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType @@ -73,7 +66,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, for ($i = 0; $i < $numberType->getValue(); $i++) { $arrayBuilder->setOffsetValueType( new ConstantIntegerType($nextIndex), - $valueType + $valueType, ); if ($nextIndex < 0) { $nextIndex = 0; @@ -85,14 +78,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } + $resultType = new ArrayType(new IntegerType(), $valueType); + if ((new ConstantIntegerType(0))->isSuperTypeOf($startIndexType)->yes()) { + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); + } if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { - return new IntersectionType([ - new ArrayType(new IntegerType(), $valueType), - new NonEmptyArrayType(), - ]); + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + if (!$isValidNumberType->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $resultType = TypeCombinator::union($resultType, new ConstantBooleanType(false)); } - return new ArrayType(new IntegerType(), $valueType); + return $resultType; } } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index 74c025957b..bf05500cba 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -4,45 +4,40 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $valueType = $scope->getType($functionCall->getArgs()[1]->value); $keysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($keysType); - if (count($constantArrays) === 0) { - return new ArrayType($keysType->getIterableValueType(), $valueType); - } - - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getValueTypes() as $keyType) { - $arrayBuilder->setOffsetValueType($keyType, $valueType); - } - $arrayTypes[] = $arrayBuilder->getArray(); + if ($keysType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return TypeCombinator::union(...$arrayTypes); + return $keysType->fillKeysArray($scope->getType($functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..6979c23759 --- /dev/null +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php @@ -0,0 +1,34 @@ +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..04ef90fef5 --- /dev/null +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -0,0 +1,350 @@ +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) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new ArrayType(new MixedType(), new 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 7c074ac65b..0000000000 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ /dev/null @@ -1,127 +0,0 @@ -getName() === 'array_filter'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - $callbackArg = $functionCall->getArgs()[1]->value ?? null; - $flagArg = $functionCall->getArgs()[2]->value ?? null; - - if ($arrayArg !== null) { - $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) { - $var = null; - $expr = null; - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - $var = $callbackArg->params[0]->var; - $expr = $statement->expr; - } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - $var = $callbackArg->params[0]->var; - $expr = $callbackArg->expr; - } - if ($var !== null && $expr !== null) { - if (!$var instanceof Variable || !is_string($var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $itemVariableName = $var->name; - if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); - } - $scope = $scope->assignVariable($itemVariableName, $itemType); - $scope = $scope->filterByTruthyValue($expr); - $itemType = $scope->getVariableType($itemVariableName); - } - } - - } else { - $keyType = new MixedType(); - $itemType = new MixedType(); - } - - return new ArrayType($keyType, $itemType); - } - - public function removeFalsey(Type $type): Type - { - $falseyTypes = StaticTypeFactory::falsey(); - - if ($type instanceof ConstantArrayType) { - $keys = $type->getKeyTypes(); - $values = $type->getValueTypes(); - - $builder = ConstantArrayTypeBuilder::createEmpty(); - - foreach ($values as $offset => $value) { - $isFalsey = $falseyTypes->isSuperTypeOf($value); - - if ($isFalsey->maybe()) { - $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); - } elseif ($isFalsey->no()) { - $builder->setOffsetValueType($keys[$offset], $value); - } - } - - return $builder->getArray(); - } - - $keyType = $type->getIterableKeyType(); - $valueType = $type->getIterableValueType(); - - $valueType = TypeCombinator::remove($valueType, $falseyTypes); - - if ($valueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - return new ArrayType($keyType, $valueType); - } - -} diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3cf1f7c9b7 --- /dev/null +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -0,0 +1,48 @@ +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..58faf9d8bf --- /dev/null +++ b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php @@ -0,0 +1,38 @@ +getName() === 'array_find_key'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + return TypeCombinator::union($arrayType->getIterableKeyType(), new NullType()); + } + +} diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index 98795c93cb..3e82909d22 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -4,49 +4,46 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; -class ArrayFlipFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_flip'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) !== 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $array = $functionCall->getArgs()[0]->value; - $argType = $scope->getType($array); - - if ($argType->isArray()->yes()) { - $keyType = $argType->getIterableKeyType(); - $itemType = $argType->getIterableValueType(); - - $itemType = ArrayType::castToArrayKeyType($itemType); - - $flippedArrayType = new ArrayType( - $itemType, - $keyType - ); - - if ($argType->isIterableAtLeastOnce()->yes()) { - $flippedArrayType = TypeCombinator::intersect($flippedArrayType, new NonEmptyArrayType()); - } - - return $flippedArrayType; + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $flipped = $arrayType->flipArray(); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($flipped, new NonEmptyArrayType()); + } + return $flipped; } } diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..c6212c02cc --- /dev/null +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -0,0 +1,64 @@ +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 e0f47e6584..2844706529 100644 --- a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -18,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'key'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index d6871e09c7..c9de826666 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -2,23 +2,34 @@ 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; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -28,10 +39,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' + return in_array($functionReflection->getName(), ['array_key_exists', 'key_exists'], true) && !$context->null(); } @@ -39,29 +50,75 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $keyType = $scope->getType($node->getArgs()[0]->value); + $key = $node->getArgs()[0]->value; + $array = $node->getArgs()[1]->value; + $keyType = $scope->getType($key)->toArrayKey(); + $arrayType = $scope->getType($array); - if ($context->truthy()) { + if ( + !$keyType instanceof ConstantIntegerType + && !$keyType instanceof ConstantStringType + ) { + if ($context->true()) { + if ($arrayType->isIterableAtLeastOnce()->no()) { + return $this->typeSpecifier->create( + $array, + new NonEmptyArrayType(), + $context, + $scope, + ); + } + + $arrayKeyType = $arrayType->getIterableKeyType(); + if ($keyType->isString()->yes()) { + $arrayKeyType = $arrayKeyType->toString(); + } elseif ($keyType->isString()->maybe()) { + $arrayKeyType = TypeCombinator::union($arrayKeyType, $arrayKeyType->toString()); + } + + $specifiedTypes = $this->typeSpecifier->create( + $key, + $arrayKeyType, + $context, + $scope, + ); + + $arrayDimFetch = new ArrayDimFetch( + $array, + $key, + ); + + return $specifiedTypes->unionWith($this->typeSpecifier->create( + $arrayDimFetch, + $arrayType->getIterableValueType(), + $context, + $scope, + ))->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); + } + + return new SpecifiedTypes(); + } + + if ($context->true()) { $type = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType($keyType) + new HasOffsetType($keyType), ); } else { $type = new HasOffsetType($keyType); } return $this->typeSpecifier->create( - $node->getArgs()[1]->value, + $array, $type, $context, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index 4e6b2dc47a..64fea966d6 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -4,14 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_first'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -31,23 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new NullType(); - continue; - } - - $keyTypes[] = $arrayKeyTypes[0]; - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getFirstIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index 3104c661b6..c3865ada3c 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -4,14 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_last'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -31,23 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new NullType(); - continue; - } - - $keyTypes[] = $arrayKeyTypes[count($arrayKeyTypes) - 1]; - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getLastIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 2c624d00c5..bb7a7bea88 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -4,42 +4,54 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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\TrinaryLogic; +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 +#[AutowiredService] +final class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'array_keys'; + return strtolower($functionReflection->getName()) === 'array_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getKeysArray(); - } - $keyType = $valueType->getIterableKeyType(); - return TypeCombinator::intersect(new ArrayType(new IntegerType(), $keyType), ...TypeUtils::getAccessoryTypes($valueType)); + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + if (count($args) >= 2) { + $filterType = $scope->getType($args[1]->value); + + $strict = TrinaryLogic::createNo(); + if (count($args) >= 3) { + $strict = $scope->getType($args[2]->value)->isTrue(); } + + return $arrayType->getKeysArrayFiltered($filterType, $strict); } - 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 543f3ac725..b58638dddf 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,21 +2,32 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; use PHPStan\Type\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 +#[AutowiredService] +final class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,54 +35,140 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_map'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { + return null; } - $valueType = new MixedType(); - $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $singleArrayArgument = !isset($functionCall->getArgs()[2]); + $callback = $functionCall->getArgs()[0]->value; + $callableType = $scope->getType($callback); + $callableIsNull = $callableType->isNull()->yes(); + if ($callableType->isCallable()->yes()) { - $valueType = new NeverType(); - foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); + $valueType = $scope->getType(new FuncCall( + $callback, + array_map( + static fn (Node\Arg $arg) => new Node\Arg(new TypeExpr($scope->getType($arg->value)->getIterableValueType())), + array_slice($functionCall->getArgs(), 1), + ), + )); + } 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(); } - $mappedArrayType = new ArrayType( - new MixedType(), - $valueType - ); $arrayType = $scope->getType($functionCall->getArgs()[1]->value); - $constantArrays = TypeUtils::getConstantArrays($arrayType); - if (!isset($functionCall->getArgs()[2])) { + if ($singleArrayArgument) { + if ($callableIsNull) { + return $arrayType; + } + $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType - ); + $totalCount = TypeCombinator::countConstantArrayValueTypes($constantArrays) * TypeCombinator::countConstantArrayValueTypes([$valueType]); + if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + foreach ($constantArrays as $constantArray) { + $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $returnedArrayBuilder->setOffsetValueType( + $keyType, + $scope->getType(new FuncCall($callback, [ + new Node\Arg(new TypeExpr($valueTypes[$i])), + ])), + $constantArray->isOptionalKey($i), + ); + } + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = TypeCombinator::intersect($returnedArray, new AccessoryArrayListType()); + } + $arrayTypes[] = $returnedArray; } - $arrayTypes[] = $returnedArrayBuilder->getArray(); - } - $mappedArrayType = TypeCombinator::union(...$arrayTypes); + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...$this->getAccessoryTypes($arrayType, $valueType)); + } } elseif ($arrayType->isArray()->yes()) { $mappedArrayType = TypeCombinator::intersect(new ArrayType( $arrayType->getIterableKeyType(), - $valueType - ), ...TypeUtils::getAccessoryTypes($arrayType)); + $valueType, + ), ...$this->getAccessoryTypes($arrayType, $valueType)); + } else { + $mappedArrayType = new ArrayType( + new MixedType(), + $valueType, + ); } } else { $mappedArrayType = TypeCombinator::intersect(new ArrayType( new IntegerType(), - $valueType - ), ...TypeUtils::getAccessoryTypes($arrayType)); + $valueType, + ), new AccessoryArrayListType(), ...$this->getAccessoryTypes($arrayType, $valueType)); } if ($arrayType->isIterableAtLeastOnce()->yes()) { @@ -81,4 +178,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $mappedArrayType; } + /** + * @return AccessoryType[] + */ + private function getAccessoryTypes(Type $arrayType, Type $valueType): array + { + $accessoryTypes = []; + foreach (TypeUtils::getAccessoryTypes($arrayType) as $accessoryType) { + if (!$accessoryType instanceof HasOffsetValueType) { + $accessoryTypes[] = $accessoryType; + continue; + } + + $accessoryTypes[] = new HasOffsetValueType($accessoryType->getOffsetType(), $valueType); + } + + return $accessoryTypes; + } + } diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 5d31918a11..1d4f7a1122 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -4,17 +4,27 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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\GeneralizePrecision; +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 +#[AutowiredService] +final class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,44 +32,106 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_merge'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; } - $keyTypes = []; - $valueTypes = []; - $nonEmpty = false; - foreach ($functionCall->getArgs() as $arg) { + $argTypes = []; + $optionalArgTypes = []; + foreach ($args as $arg) { $argType = $scope->getType($arg->value); + if ($arg->unpack) { - $argType = $argType->getIterableValueType(); - if ($argType instanceof UnionType) { - foreach ($argType->getTypes() as $innerType) { - $argType = $innerType; + if ($argType->isConstantArray()->yes()) { + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $argTypes[] = $valueType; + } + } + } else { + $argTypes[] = $argType->getIterableValueType(); + } + + if (!$argType->isIterableAtLeastOnce()->yes()) { + // unpacked params can be empty, making them optional + $optionalArgTypesOffset = count($argTypes) - 1; + foreach (array_keys($argTypes) as $key) { + $optionalArgTypes[] = $optionalArgTypesOffset + $key; } } + } else { + $argTypes[] = $argType; } + } + + $allConstant = TrinaryLogic::createYes()->lazyAnd( + $argTypes, + static fn (Type $argType) => $argType->isConstantArray(), + ); + + 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; + } + } - $keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType(), GeneralizePrecision::moreSpecific()); + foreach ($keyTypes as $keyType) { + $newArrayBuilder->setOffsetValueType( + $keyType instanceof ConstantIntegerType ? null : $keyType, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), + ); + } + } + + return $newArrayBuilder->getArray(); + } + + $keyTypes = []; + $valueTypes = []; + $nonEmpty = false; + $isList = true; + foreach ($argTypes as $key => $argType) { + $keyType = $argType->getIterableKeyType(); + $keyTypes[] = $keyType; $valueTypes[] = $argType->getIterableValueType(); - if (!$argType->isIterableAtLeastOnce()->yes()) { + if (!(new IntegerType())->isSuperTypeOf($keyType)->yes()) { + $isList = false; + } + + if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) { continue; } $nonEmpty = true; } + $keyType = TypeCombinator::union(...$keyTypes); + if ($keyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + $arrayType = new ArrayType( - TypeCombinator::union(...$keyTypes), - TypeCombinator::union(...$valueTypes) + $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 index 8f46a6d47c..a0f49b4ca3 100644 --- a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -4,13 +4,16 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class ArrayNextDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayNextDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -18,10 +21,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['next', 'prev'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayPadDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPadDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..896034f32f --- /dev/null +++ b/src/Type/Php/ArrayPadDynamicReturnTypeExtension.php @@ -0,0 +1,56 @@ +getName() === 'array_pad'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[2])) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $itemType = $scope->getType($functionCall->getArgs()[2]->value); + + $returnType = new ArrayType( + TypeCombinator::union($arrayType->getIterableKeyType(), new IntegerType()), + TypeCombinator::union($arrayType->getIterableValueType(), $itemType), + ); + + $lengthType = $scope->getType($functionCall->getArgs()[1]->value); + if ( + $arrayType->isIterableAtLeastOnce()->yes() + || $lengthType->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + + if ($arrayType->isList()->yes()) { + $returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType()); + } + + return $returnType; + } + +} diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index 1e7ed9d642..eef5642028 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -4,14 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -28,11 +31,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -41,27 +44,9 @@ public function getTypeFromFunctionCall( return new ConstantBooleanType(false); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new ConstantBooleanType(false); - continue; - } - - $valueOffset = $functionReflection->getName() === 'reset' - ? $arrayKeyTypes[0] - : $arrayKeyTypes[count($arrayKeyTypes) - 1]; - - $keyTypes[] = $constantArray->getOffsetValueType($valueOffset); - } - - return TypeCombinator::union(...$keyTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $functionReflection->getName() === 'reset' + ? $argType->getFirstIterableValueType() + : $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 1e4c2e97b6..61bfdf69e6 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -4,14 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_pop'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -31,23 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $valueTypes[] = new NullType(); - continue; - } - - $valueTypes[] = $constantArray->getOffsetValueType($arrayKeyTypes[count($arrayKeyTypes) - 1]); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 7172838fb1..f61d48033e 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -4,17 +4,21 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; -class ArrayRandFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,16 +26,16 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_rand'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); - if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if ($argsCount < 1) { + return null; } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $isInteger = (new IntegerType())->isSuperTypeOf($firstArgType->getIterableKeyType()); - $isString = (new StringType())->isSuperTypeOf($firstArgType->getIterableKeyType()); + $isInteger = $firstArgType->getIterableKeyType()->isInteger(); + $isString = $firstArgType->getIterableKeyType()->isString(); if ($isInteger->yes()) { $valueType = new IntegerType(); @@ -47,14 +51,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($secondArgType instanceof ConstantIntegerType) { - if ($secondArgType->getValue() === 1) { - return $valueType; - } + $one = new ConstantIntegerType(1); + if ($one->isSuperTypeOf($secondArgType)->yes()) { + return $valueType; + } - if ($secondArgType->getValue() >= 2) { - return new ArrayType(new IntegerType(), $valueType); - } + $bigger2 = IntegerRangeType::fromInterval(2, null); + if ($bigger2->isSuperTypeOf($secondArgType)->yes()) { + return new ArrayType(new IntegerType(), $valueType); } return TypeCombinator::union($valueType, new ArrayType(new IntegerType(), $valueType)); diff --git a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php index 0d7462c6e3..7f955e6baa 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -4,14 +4,18 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,21 +23,21 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_reduce'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[1])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackType = $scope->getType($functionCall->getArgs()[1]->value); if ($callbackType->isCallable()->no()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, $functionCall->getArgs(), - $callbackType->getCallableParametersAcceptors($scope) + $callbackType->getCallableParametersAcceptors($scope), )->getReturnType(); if (isset($functionCall->getArgs()[2])) { @@ -43,20 +47,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arraysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($arraysType); + $constantArrays = $arraysType->getConstantArrays(); if (count($constantArrays) > 0) { - $onlyEmpty = true; - $onlyNonEmpty = true; + $onlyEmpty = TrinaryLogic::createYes(); + $onlyNonEmpty = TrinaryLogic::createYes(); foreach ($constantArrays as $constantArray) { - $isEmpty = count($constantArray->getValueTypes()) === 0; - $onlyEmpty = $onlyEmpty && $isEmpty; - $onlyNonEmpty = $onlyNonEmpty && !$isEmpty; + $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); + $onlyEmpty = $onlyEmpty->and($iterableAtLeastOnce->negate()); + $onlyNonEmpty = $onlyNonEmpty->and($iterableAtLeastOnce); } - if ($onlyEmpty) { + if ($onlyEmpty->yes()) { return $initialType; } - if ($onlyNonEmpty) { + if ($onlyNonEmpty->yes()) { return $callbackReturnType; } } diff --git a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..fdb8066f60 --- /dev/null +++ b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php @@ -0,0 +1,140 @@ +getName()) === 'array_replace'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; + } + + $argTypes = []; + $optionalArgTypes = []; + foreach ($args as $arg) { + $argType = $scope->getType($arg->value); + + if ($arg->unpack) { + 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, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), + ); + } + } + + return $newArrayBuilder->getArray(); + } + + $keyTypes = []; + $valueTypes = []; + $nonEmpty = false; + $isList = true; + foreach ($argTypes as $key => $argType) { + $keyType = $argType->getIterableKeyType(); + $keyTypes[] = $keyType; + $valueTypes[] = $argType->getIterableValueType(); + + if (!$argType->isList()->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([], []); + } + + $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/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 37421aa683..0c90c7bfaa 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -4,26 +4,42 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_reverse'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - return $scope->getType($functionCall->getArgs()[0]->value); + $type = $scope->getType($functionCall->getArgs()[0]->value); + if ($type->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new ConstantBooleanType(false); + + return $type->reverseArray($preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index db63ff4d31..b392e91340 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -4,160 +4,50 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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; +#[AutowiredService] final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_search'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $haystackArgType = $scope->getType($functionCall->getArgs()[1]->value); - $haystackIsArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($haystackArgType); - if ($haystackIsArray->no()) { - return new NullType(); + if ($haystackArgType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($argsCount < 3) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); - } - - $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); - if (!($strictArgType instanceof ConstantBooleanType)) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false), new NullType()); - } elseif ($strictArgType->getValue() === false) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); + $strictArgType = new ConstantBooleanType(false); + } else { + $strictArgType = $scope->getType($functionCall->getArgs()[2]->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, $strictArgType->isTrue()); } } diff --git a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..38d8909e9b --- /dev/null +++ b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php @@ -0,0 +1,58 @@ +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 6adf0db00d..b961e624e0 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -4,14 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +20,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_shift'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -31,23 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $valueTypes[] = new NullType(); - continue; - } - - $valueTypes[] = $constantArray->getOffsetValueType($arrayKeyTypes[0]); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getFirstIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 316e5f06bb..5ac2ba4606 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -4,85 +4,46 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; } - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope - ): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - - if ($arrayArg === null) { - return new ArrayType( - new IntegerType(), - new MixedType() - ); - } - - $valueType = $scope->getType($arrayArg); - - if (isset($functionCall->getArgs()[1])) { - $offset = $scope->getType($functionCall->getArgs()[1]->value); - if (!$offset instanceof ConstantIntegerType) { - $offset = new ConstantIntegerType(0); - } - } else { - $offset = new ConstantIntegerType(0); - } - - if (isset($functionCall->getArgs()[2])) { - $limit = $scope->getType($functionCall->getArgs()[2]->value); - if (!$limit instanceof ConstantIntegerType) { - $limit = new NullType(); - } - } else { - $limit = new NullType(); - } - - $constantArrays = TypeUtils::getConstantArrays($valueType); - if (count($constantArrays) === 0) { - $arrays = TypeUtils::getArrays($valueType); - if (count($arrays) !== 0) { - return TypeCombinator::union(...$arrays); - } - return new ArrayType( - new MixedType(), - new MixedType() - ); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - if (isset($functionCall->getArgs()[3])) { - $preserveKeys = $scope->getType($functionCall->getArgs()[3]->value); - $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeys)->yes(); - } else { - $preserveKeys = false; + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $arrayTypes = array_map(static 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 index 7024a7d64e..5dd3c94174 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -4,14 +4,24 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; +use PHPStan\TrinaryLogic; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function count; -class ArraySpliceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_splice'; @@ -20,16 +30,23 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $arrayArg = $scope->getType($functionCall->getArgs()[0]->value); + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); - return new ArrayType($arrayArg->getIterableKeyType(), $arrayArg->getIterableValueType()); + return $arrayType->sliceArray($offsetType, $lengthType, TrinaryLogic::createNo()); } } diff --git a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php index 03a5d15910..061f3d4dff 100644 --- a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,22 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Mul; +use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Scalar\Int_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use function count; +#[AutowiredService] final class ArraySumFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -22,34 +26,42 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_sum'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); - $itemType = $arrayType->getIterableValueType(); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $resultTypes = []; - if ($arrayType->isIterableAtLeastOnce()->no()) { - return new ConstantIntegerType(0); - } + if (count($argType->getConstantArrays()) > 0) { + foreach ($argType->getConstantArrays() as $constantArray) { + $node = new Int_(0); - $intUnionFloat = new UnionType([new IntegerType(), new FloatType()]); + foreach ($constantArray->getValueTypes() as $i => $type) { + if ($constantArray->isOptionalKey($i)) { + $node = new Plus($node, new TypeExpr(TypeCombinator::union($type, new ConstantIntegerType(0)))); + } else { + $node = new Plus($node, new TypeExpr($type)); + } + } - if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return $itemType; + $resultTypes[] = $scope->getType($node); } + } else { + $itemType = $argType->getIterableValueType(); + + $mulNode = new Mul(new TypeExpr($itemType), new TypeExpr(IntegerRangeType::fromInterval(0, null))); - return $intUnionFloat; + $resultTypes[] = $scope->getType(new Plus(new TypeExpr($itemType), $mulNode)); } - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return TypeCombinator::union(new ConstantIntegerType(0), $itemType); + if (!$argType->isIterableAtLeastOnce()->yes()) { + $resultTypes[] = new ConstantIntegerType(0); } - return TypeCombinator::union(new ConstantIntegerType(0), $intUnionFloat); + return TypeCombinator::union(...$resultTypes)->toNumber(); } } diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 82c037f45c..794b8df7ba 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -4,40 +4,41 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'array_values'; + return strtolower($functionReflection->getName()) === 'array_values'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getValuesArray(); - } - return TypeCombinator::intersect(new ArrayType(new IntegerType(), $valueType->getIterableValueType()), ...TypeUtils::getAccessoryTypes($valueType)); - } + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return new ArrayType( - new IntegerType(), - new MixedType() - ); + return $arrayType->getValuesArray(); } } diff --git a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php index b47af84fd5..adc436e584 100644 --- a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php @@ -8,13 +8,15 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\FunctionTypeSpecifyingExtension; -class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { diff --git a/src/Type/Php/AssertThrowTypeExtension.php b/src/Type/Php/AssertThrowTypeExtension.php new file mode 100644 index 0000000000..b30bab71cc --- /dev/null +++ b/src/Type/Php/AssertThrowTypeExtension.php @@ -0,0 +1,38 @@ +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..5ee4ab5cf0 --- /dev/null +++ b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php @@ -0,0 +1,102 @@ +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 bf204f5e01..bf91435e62 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -4,15 +4,18 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { if (!isset($functionCall->getArgs()[1])) { @@ -36,8 +39,8 @@ public function getTypeFromFunctionCall( return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]); } - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new UnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..8539349a15 --- /dev/null +++ b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php @@ -0,0 +1,69 @@ +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->isFloat()->yes()) { + return new ErrorType(); + } + + if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) { + return new BooleanType(); + } + + if ($operatorSigil === '<=>') { + return IntegerRangeType::fromInterval(-1, 1); + } + + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $bcMathNumberType->isSuperTypeOf($otherSide)->yes() + ) { + return $bcMathNumberType; + } + + return new ErrorType(); + } + +} diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index 0c1bf2e22b..e34e4d3859 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -5,12 +5,15 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\UnaryMinus; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -19,9 +22,14 @@ use function in_array; use function is_numeric; -class BcMathStringOrNullReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['bcdiv', 'bcmod', 'bcpowmod', 'bcsqrt'], true); @@ -39,16 +47,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); - $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); - if (isset($functionCall->getArgs()[1]) === false) { - return $stringAndNumericStringType; + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new NullType(); + } + + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + $defaultReturnType = $stringAndNumericStringType; + } else { + $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); } $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); - $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument instanceof IntegerType; + $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument->isInteger()->yes(); if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } @@ -61,12 +81,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $thirdArgument = $scope->getType($functionCall->getArgs()[2]->value); - $thirdArgumentIsNumeric = ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) || $thirdArgument instanceof IntegerType; + $thirdArgumentIsNumeric = false; + $thirdArgumentIsNegative = false; + if ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) { + $thirdArgumentIsNumeric = true; + $thirdArgumentIsNegative = ($thirdArgument->getValue() < 0); + } elseif ($thirdArgument->isInteger()->yes()) { + $thirdArgumentIsNumeric = true; + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($thirdArgument)->yes()) { + $thirdArgumentIsNegative = true; + } + } if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $thirdArgumentIsNegative) { + return new NeverType(); + } + if (($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) && $thirdArgumentIsNumeric) { return $stringAndNumericStringType; } @@ -79,16 +117,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, * https://www.php.net/manual/en/function.bcsqrt.php * > Returns the square root as a string, or NULL if operand is negative. * - * @param FuncCall $functionCall - * @param Scope $scope - * @return Type */ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type { $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); - $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); + if ($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; } @@ -97,8 +140,11 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $firstArgumentIsPositive = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() >= 0; $firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0; - if ($firstArgument instanceof UnaryMinus || - ($firstArgumentIsNegative)) { + if ($firstArgument instanceof UnaryMinus || $firstArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } @@ -113,11 +159,22 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); $secondArgumentIsValid = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && !$this->isZero($secondArgument->getValue()); $secondArgumentIsNonNumeric = $secondArgument instanceof ConstantScalarType && !is_numeric($secondArgument->getValue()); + $secondArgumentIsNegative = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && $secondArgument->getValue() < 0; if ($secondArgumentIsNonNumeric) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } + if ($secondArgument instanceof UnaryMinus || $secondArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + } + if ($firstArgumentIsPositive && $secondArgumentIsValid) { return $stringAndNumericStringType; } @@ -129,19 +186,32 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type * bcpowmod() * https://www.php.net/manual/en/function.bcpowmod.php * > Returns the result as a string, or FALSE if modulus is 0 or exponent is negative. - * @param FuncCall $functionCall - * @param Scope $scope - * @return Type */ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type { + 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) { @@ -149,6 +219,10 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type } if ($exponentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); } @@ -158,12 +232,24 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); if ($modulusIsZero || $modulusIsNonNumeric) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); } if ($modulus instanceof ConstantScalarType) { return $stringAndNumericStringType; } + } else { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + } + + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $stringAndNumericStringType; } return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]); @@ -173,7 +259,6 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type * Utility to help us determine if value is zero. Handles cases where we pass "0.000" too. * * @param mixed $value - * @return bool */ private function isZero($value): bool { diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 771c5f924f..53cec9a0e6 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -11,15 +11,19 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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\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 +#[AutowiredService] +final class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -27,42 +31,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->getArgs()[0]) && $context->truthy(); + 'enum_exists', + ], true) && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $argType = $scope->getType($node->getArgs()[0]->value); - $classStringType = new ClassStringType(); - if (TypeCombinator::intersect($argType, $classStringType) instanceof NeverType) { - if ($argType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('class_exists'), [ - new Arg(new String_(ltrim($argType->getValue(), '\\'))), - ]), - new ConstantBooleanType(true), - $context, - false, - $scope - ); - } + 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->getArgs()[0]->value, - $classStringType, + $narrowedType, $context, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3d1010aff0 --- /dev/null +++ b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php @@ -0,0 +1,69 @@ +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 e8d12490e3..580595acd7 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -2,19 +2,22 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -22,11 +25,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'bind'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { $closureType = $scope->getType($methodCall->getArgs()[0]->value); if (!($closureType instanceof ClosureType)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return $closureType; diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 1ea706a5ec..3e275677e2 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -2,19 +2,22 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -22,11 +25,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 925b272ddd..df0e2d54aa 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -2,21 +2,26 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; +use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ClosureFromCallableDynamicReturnTypeExtension implements \PHPStan\Type\DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -24,14 +29,10 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'fromCallable'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); + return null; } $callableType = $scope->getType($methodCall->getArgs()[0]->value); @@ -45,7 +46,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(), + throwPoints: $variant->getThrowPoints(), + impurePoints: $variant->getImpurePoints(), + invalidateExpressions: $variant->getInvalidateExpressions(), + usedVariables: $variant->getUsedVariables(), + acceptsNamedArguments: $variant->acceptsNamedArguments(), + mustUseReturnValue: $variant->mustUseReturnValue(), ); } diff --git a/src/Type/Php/CompactFunctionReturnTypeExtension.php b/src/Type/Php/CompactFunctionReturnTypeExtension.php index 08a11322e7..0d7f40c9f7 100644 --- a/src/Type/Php/CompactFunctionReturnTypeExtension.php +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -4,22 +4,26 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use function array_merge; +use function count; -class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private bool $checkMaybeUndefinedVariables; - - public function __construct(bool $checkMaybeUndefinedVariables) + public function __construct( + #[AutowiredParameter] + private bool $checkMaybeUndefinedVariables, + ) { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,16 +34,15 @@ 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 (count($functionCall->getArgs()) === 0) { - return $defaultReturnType; + return null; } if ($scope->canAnyVariableExist() && !$this->checkMaybeUndefinedVariables) { - return $defaultReturnType; + return null; } $array = ConstantArrayTypeBuilder::createEmpty(); @@ -47,7 +50,7 @@ public function getTypeFromFunctionCall( $type = $scope->getType($arg->value); $constantStrings = $this->findConstantStrings($type); if ($constantStrings === null) { - return $defaultReturnType; + return null; } foreach ($constantStrings as $constantString) { $has = $scope->hasVariableType($constantString->getValue()); @@ -63,7 +66,6 @@ public function getTypeFromFunctionCall( } /** - * @param Type $type * @return array|null */ private function findConstantStrings(Type $type): ?array diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..f332c99fc9 --- /dev/null +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -0,0 +1,57 @@ +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..16e55861bc --- /dev/null +++ b/src/Type/Php/ConstantHelper.php @@ -0,0 +1,44 @@ += 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..70d08ba9e0 --- /dev/null +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,64 @@ +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 a506d1945a..b87d34c466 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -4,15 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\IntegerRangeType; +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 +#[AutowiredService] +final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,35 +25,21 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) > 1) { $mode = $scope->getType($functionCall->getArgs()[1]->value); - if ($mode->isSuperTypeOf(new ConstantIntegerType(\COUNT_RECURSIVE))->yes()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { + return null; } } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); - if (count($constantArrays) === 0) { - if ($argType->isIterableAtLeastOnce()->yes()) { - return IntegerRangeType::fromInterval(1, null); - } - - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - $countTypes = []; - foreach ($constantArrays as $array) { - $countTypes[] = $array->count(); - } - - return TypeCombinator::union(...$countTypes); + return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize(); } } diff --git a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php index be461f08d8..741c7cc880 100644 --- a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php @@ -8,21 +8,23 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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() @@ -34,14 +36,14 @@ 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->getArgs()[0]->value))->yes()) { + if (!$scope->getType($node->getArgs()[0]->value)->isArray()->yes()) { return new SpecifiedTypes([], []); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..04daf2ed55 --- /dev/null +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -0,0 +1,88 @@ +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..9224db2dbe --- /dev/null +++ b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,236 @@ +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); + $integerFalseType = TypeCombinator::union($integerType, $falseType); + $stringListType = TypeCombinator::intersect(new ArrayType($integerType, $stringType), new AccessoryArrayListType()); + $nestedArrayInListType = TypeCombinator::intersect(new ArrayType($integerType, new ArrayType($stringType, $stringType)), new AccessoryArrayListType()); + $mixedType = new MixedType(); + + $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' => $stringFalseType, + 'CURLINFO_PRIMARY_IP' => $stringType, + 'CURLINFO_PRIMARY_PORT' => $integerType, + 'CURLINFO_LOCAL_IP' => $stringType, + 'CURLINFO_LOCAL_PORT' => $integerType, + 'CURLINFO_SIZE_UPLOAD' => $floatType, + 'CURLINFO_SIZE_DOWNLOAD' => $floatType, + 'CURLINFO_SPEED_DOWNLOAD' => $floatType, + 'CURLINFO_SPEED_UPLOAD' => $floatType, + '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' => $mixedType, + 'CURLINFO_RESPONSE_CODE' => $integerType, + 'CURLINFO_HTTP_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' => $stringListType, + 'CURLINFO_COOKIELIST' => $stringListType, + 'CURLINFO_FTP_ENTRY_PATH' => $stringFalseType, + 'CURLINFO_APPCONNECT_TIME' => $floatType, + 'CURLINFO_CERTINFO' => $nestedArrayInListType, + 'CURLINFO_CONDITION_UNMET' => $integerType, + 'CURLINFO_RTSP_CLIENT_CSEQ' => $integerType, + 'CURLINFO_RTSP_CSEQ_RECV' => $integerType, + 'CURLINFO_RTSP_SERVER_CSEQ' => $integerType, + 'CURLINFO_RTSP_SESSION_ID' => $stringFalseType, + 'CURLINFO_HTTP_VERSION' => $integerType, + 'CURLINFO_PROTOCOL' => $integerType, + '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, + 'CURLINFO_EFFECTIVE_METHOD' => $stringType, + 'CURLINFO_PROXY_ERROR' => $integerType, + 'CURLINFO_REFERER' => $stringFalseType, + 'CURLINFO_RETRY_AFTER' => $integerType, + 'CURLINFO_CAINFO' => $stringFalseType, + 'CURLINFO_CAPATH' => $stringFalseType, + 'CURLINFO_POSTTRANSFER_TIME_T' => $integerFalseType, + 'CURLINFO_QUEUE_TIME_T' => $integerFalseType, + 'CURLINFO_USED_PROXY' => $integerFalseType, + 'CURLINFO_HTTPAUTH_USED' => $integerFalseType, + 'CURLINFO_PROXYAUTH_USED' => $integerFalseType, + 'CURLINFO_CONN_ID' => $integerFalseType, + ]; + + 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()); + $nestedArrayInListType = TypeCombinator::intersect(new ArrayType($integerType, new ArrayType($stringType, $stringType)), new AccessoryArrayListType()); + + $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' => $nestedArrayInListType, + 'primary_port' => $integerType, + 'local_ip' => $stringType, + 'local_port' => $integerType, + 'http_version' => $integerType, + 'protocol' => $integerType, + 'ssl_verifyresult' => $integerType, + 'scheme' => $stringType, + 'appconnect_time_us' => $integerType, + 'queue_time_us' => $integerType, + 'connect_time_us' => $integerType, + 'namelookup_time_us' => $integerType, + 'pretransfer_time_us' => $integerType, + 'redirect_time_us' => $integerType, + 'starttransfer_time_us' => $integerType, + 'posttransfer_time_us' => $integerType, + 'total_time_us' => $integerType, + 'request_header' => $stringType, + 'effective_method' => $stringType, + 'capath' => $stringType, + 'cainfo' => $stringType, + 'used_proxy' => $integerType, + 'httpauth_used' => $integerType, + 'proxyauth_used' => $integerType, + 'conn_id' => $integerType, + ]; + 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 f837c7cb7b..0000000000 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ /dev/null @@ -1,35 +0,0 @@ -getName() === 'curl_init'; - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope - ): Type - { - $argsCount = count($functionCall->getArgs()); - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if ($argsCount === 0) { - return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); - } - - return $returnType; - } - -} diff --git a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..666a376cde --- /dev/null +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -0,0 +1,43 @@ +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..55d9e28d74 --- /dev/null +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -0,0 +1,45 @@ +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 index 5821cd1cea..c74867e186 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -4,17 +4,20 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use function count; -class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date'; @@ -23,29 +26,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return new StringType(); - } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($argType); - if (count($constantStrings) === 0) { - return new StringType(); - } - - foreach ($constantStrings as $constantString) { - $formattedDate = date($constantString->getValue()); - if (!is_numeric($formattedDate)) { - return new StringType(); - } + return null; } - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + 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..bac485292b --- /dev/null +++ b/src/Type/Php/DateFunctionReturnTypeHelper.php @@ -0,0 +1,120 @@ +getConstantStrings() as $formatString) { + $types[] = $this->buildReturnTypeFromFormat($formatString->getValue(), $useMicrosec); + } + + if (count($types) === 0) { + $types[] = $formatType->isNonEmptyString()->yes() + ? new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]) + : new StringType(); + } + + $type = TypeCombinator::union(...$types); + + if ($type->isNumericString()->no() && $formatType->isNonEmptyString()->yes()) { + $type = TypeCombinator::union($type, new IntersectionType([ + new StringType(), new AccessoryNonEmptyStringType(), + ])); + } + + return $type; + } + + public function buildReturnTypeFromFormat(string $formatString, bool $useMicrosec): Type + { + // see see https://www.php.net/manual/en/datetime.format.php + switch ($formatString) { + case 'd': + return $this->buildNumericRangeType(1, 31, true); + case 'j': + return $this->buildNumericRangeType(1, 31, false); + case 'N': + return $this->buildNumericRangeType(1, 7, false); + case 'w': + return $this->buildNumericRangeType(0, 6, false); + case 'm': + return $this->buildNumericRangeType(1, 12, true); + case 'n': + return $this->buildNumericRangeType(1, 12, false); + case 't': + return $this->buildNumericRangeType(28, 31, false); + case 'L': + return $this->buildNumericRangeType(0, 1, false); + case 'g': + return $this->buildNumericRangeType(1, 12, false); + case 'G': + return $this->buildNumericRangeType(0, 23, false); + case 'h': + return $this->buildNumericRangeType(1, 12, true); + case 'H': + return $this->buildNumericRangeType(0, 23, true); + case 'I': + return $this->buildNumericRangeType(0, 1, false); + case 'u': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType(), new AccessoryNumericStringType()]) + : new ConstantStringType('000000'); + case 'v': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType(), new AccessoryNumericStringType()]) + : new ConstantStringType('000'); + } + + $date = date($formatString); + + // If parameter string is not included, returned as ConstantStringType + if ($date === $formatString) { + return new ConstantStringType($date); + } + + if (is_numeric($date)) { + return new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + } + + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } + + private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type + { + $types = []; + + for ($i = $min; $i <= $max; $i++) { + $string = (string) $i; + + if ($zeroPad) { + $string = str_pad($string, 2, '0', STR_PAD_LEFT); + } + + $types[] = new ConstantStringType($string); + } + + return new UnionType($types); + } + +} diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 4a31169303..971f593bfc 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -5,16 +5,24 @@ use DateInterval; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function count; -class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; @@ -27,23 +35,32 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { - new \DateInterval($constantString->getValue()); + new DateInterval($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedIntervalStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..c5f597fcdc --- /dev/null +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -0,0 +1,70 @@ +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/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..f6d32b02ed --- /dev/null +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -0,0 +1,91 @@ +getName() === 'format'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + + if (!isset($arguments[0])) { + return null; + } + + $arg = $scope->getType($arguments[0]->value); + + $constantStrings = $arg->getConstantStrings(); + if (count($constantStrings) === 0) { + if ($arg->isNonEmptyString()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return null; + } + + // The worst case scenario for the non-falsy-string check is that every number is 0. + $dateInterval = new DateInterval('P0D'); + + $possibleReturnTypes = []; + foreach ($constantStrings as $string) { + $value = $dateInterval->format($string->getValue()); + + $accessories = []; + if (is_numeric($value)) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($value !== '0' && $value !== '') { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($value !== '') { + $accessories[] = new AccessoryNonEmptyStringType(); + } + if (strtolower($value) === $value) { + $accessories[] = new AccessoryLowercaseStringType(); + } + if (strtoupper($value) === $value) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + if (count($accessories) === 0) { + return null; + } + + $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); + } + + return TypeCombinator::union(...$possibleReturnTypes); + } + +} diff --git a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php new file mode 100644 index 0000000000..b6f2acd839 --- /dev/null +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -0,0 +1,86 @@ +getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (!$methodCall->class instanceof Name) { + return null; + } + + $className = $scope->resolveName($methodCall->class); + if (strtolower($className) !== 'dateperiod') { + return null; + } + + if (!isset($methodCall->getArgs()[0])) { + return null; + } + + $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 null; + } + +} diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index a07800298d..2facd03945 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -6,16 +6,25 @@ use DateTimeImmutable; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function count; +use function in_array; -class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); @@ -28,23 +37,32 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { - new \DateTime($constantString->getValue()); + new DateTime($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..c2782272cf --- /dev/null +++ b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php @@ -0,0 +1,51 @@ +getName(), ['date_create', 'date_create_immutable'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $datetimes = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + + if (count($datetimes) === 0) { + return null; + } + + $types = []; + $className = $functionReflection->getName() === 'date_create' ? DateTime::class : DateTimeImmutable::class; + foreach ($datetimes as $constantString) { + $isValid = date_create($constantString->getValue()) !== false; + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php index 988a91ff82..eedc6a2fee 100644 --- a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -6,15 +6,18 @@ use DateTimeImmutable; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; +use function in_array; -class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,25 +25,29 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['date_create_from_format', 'date_create_immutable_from_format'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $format = $scope->getType($functionCall->getArgs()[0]->value); - $datetime = $scope->getType($functionCall->getArgs()[1]->value); + $formats = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $datetimes = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); - if (!$format instanceof ConstantStringType || !$datetime instanceof ConstantStringType) { - return $defaultReturnType; + if (count($formats) === 0 || count($datetimes) === 0) { + return null; } - $isValid = (DateTime::createFromFormat($format->getValue(), $datetime->getValue()) !== false); - + $types = []; $className = $functionReflection->getName() === 'date_create_from_format' ? DateTime::class : DateTimeImmutable::class; - return $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + foreach ($formats as $formatConstantString) { + foreach ($datetimes as $datetimeConstantString) { + $isValid = (DateTime::createFromFormat($formatConstantString->getValue(), $datetimeConstantString->getValue()) !== false); + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + } + + return TypeCombinator::union(...$types); } } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php new file mode 100644 index 0000000000..02c0099c4e --- /dev/null +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -0,0 +1,73 @@ +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..fc92e2b91d --- /dev/null +++ b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php @@ -0,0 +1,45 @@ +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..0c4c0bd9dd --- /dev/null +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -0,0 +1,66 @@ +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 f1fbcc3e27..d71df350c1 100644 --- a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php @@ -2,17 +2,21 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; 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\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; -class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -25,7 +29,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'define' @@ -37,7 +41,7 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { $constantName = $scope->getType($node->getArgs()[0]->value); @@ -48,15 +52,20 @@ public function specifyTypes( return new SpecifiedTypes([], []); } + $valueType = $scope->getType($node->getArgs()[1]->value); + $finalType = $scope->getConstantExplicitTypeFromConfig( + $constantName->getValue(), + $valueType, + ); + 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->getArgs()[1]->value), + $finalType, TypeSpecifierContext::createTruthy(), - false, - $scope - ); + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 12458f0093..df0fe21f2b 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -8,16 +8,23 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; +use function count; -class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +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,19 +33,19 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'defined' && count($node->getArgs()) >= 1 - && !$context->null(); + && $context->true(); } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { $constantName = $scope->getType($node->getArgs()[0]->value); @@ -49,14 +56,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, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php index a134c6d4a2..e6ae81a8d4 100644 --- a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php @@ -4,14 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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..8ade76245a --- /dev/null +++ b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php @@ -0,0 +1,34 @@ +getDeclaringClass()->getName() === 'Ds\Map' + && in_array($methodReflection->getName(), ['get', 'remove'], true); + } + + 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 d4a5719487..6b833656d7 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.php @@ -4,15 +4,15 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use PHPStan\Type\TypeWithClassName; +use function count; +use function in_array; +#[AutowiredService] final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -23,54 +23,41 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'; + return in_array($methodReflection->getName(), ['get', 'remove'], true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $returnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); - - if (count($methodCall->getArgs()) > 1) { - return $returnType; + $argsCount = count($methodCall->getArgs()); + if ($argsCount > 1) { + return null; } - if ($returnType instanceof UnionType) { - $types = array_values( - array_filter( - $returnType->getTypes(), - static function (Type $type): bool { - if ( - $type instanceof TemplateType - && $type->getName() === 'TDefault' - && ( - $type->getScope()->equals(TemplateTypeScope::createWithMethod('Ds\Map', 'get')) - || $type->getScope()->equals(TemplateTypeScope::createWithMethod('Ds\Map', 'remove')) - ) - ) { - return false; - } + if ($argsCount === 0) { + return null; + } - return true; - } - ) - ); + $mapType = $scope->getType($methodCall->var); + if (!$mapType instanceof TypeWithClassName) { + return null; + } - if (count($types) === 1) { - return $types[0]; - } + $mapAncestor = $mapType->getAncestorWithClassName('Ds\Map'); + if ($mapAncestor === null) { + return null; + } - if (count($types) === 0) { - return $returnType; - } + $mapAncestorClass = $mapAncestor->getClassReflection(); + if ($mapAncestorClass === null) { + return null; + } - return TypeCombinator::union(...$types); + $valueType = $mapAncestorClass->getActiveTemplateTypeMap()->getType('TValue'); + if ($valueType === null) { + return null; } - return $returnType; + return $valueType; } } diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index f7307652cb..65cf25e8d5 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -4,30 +4,34 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -38,35 +42,52 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); - $isSuperset = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); - if ($isSuperset->yes()) { - if ($this->phpVersion->getVersionId() >= 80000) { + $delimiterType = $scope->getType($args[0]->value); + $isEmptyString = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); + if ($isEmptyString->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); - } elseif ($isSuperset->no()) { - $arrayType = new ArrayType(new IntegerType(), new StringType()); - if ( - !isset($functionCall->getArgs()[2]) - || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->getArgs()[2]->value))->yes() - ) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); - } + } + + $stringType = $scope->getType($args[1]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $returnValueType = new IntersectionType($accessory); + } else { + $returnValueType = new StringType(); + } + + $returnType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType), new AccessoryArrayListType()); + if ( + !isset($args[2]) + || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes() + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } - return $arrayType; + if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($delimiterType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($returnType); + $returnType = TypeUtils::toBenevolentUnion($returnType); } return $returnType; diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..cb2e93acdf --- /dev/null +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -0,0 +1,485 @@ +|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('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'); + if ($filterValue === null) { + return $mixedType; + } + } 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('FILTER_NULL_ON_FAILURE', $flagsType) + ? new NullType() + : new ConstantBooleanType(false)); + + $inputIsArray = $inputType->isArray(); + $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); + if ($inputIsArray->no() && $hasRequireArrayFlag) { + return $defaultType; + } + + $hasForceArrayFlag = $this->hasFlag('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()); + + $map = [ + '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_DOMAIN' => $stringType, + 'FILTER_VALIDATE_EMAIL' => $nonFalsyStringType, + 'FILTER_VALIDATE_FLOAT' => $floatType, + 'FILTER_VALIDATE_INT' => $intType, + 'FILTER_VALIDATE_IP' => $nonFalsyStringType, + 'FILTER_VALIDATE_MAC' => $nonFalsyStringType, + 'FILTER_VALIDATE_REGEXP' => $stringType, + 'FILTER_VALIDATE_URL' => $nonFalsyStringType, + ]; + + $this->filterTypeMap = []; + foreach ($map as $filter => $type) { + $constant = $this->getConstant($filter); + if ($constant === null) { + continue; + } + $this->filterTypeMap[$constant] = $type; + } + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { + $sanitizeMagicQuote = $this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES'); + if ($sanitizeMagicQuote !== null) { + $this->filterTypeMap[$sanitizeMagicQuote] = $stringType; + } + } + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { + $sanitizeAddSlashes = $this->getConstant('FILTER_SANITIZE_ADD_SLASHES'); + if ($sanitizeAddSlashes !== null) { + $this->filterTypeMap[$sanitizeAddSlashes] = $stringType; + } + } + + return $this->filterTypeMap; + } + + /** + * @return array> + */ + private function getFilterTypeOptions(): array + { + if ($this->filterTypeOptions !== null) { + return $this->filterTypeOptions; + } + + $map = [ + 'FILTER_VALIDATE_INT' => ['min_range', 'max_range'], + // PHPStan does not yet support FloatRangeType + // 'FILTER_VALIDATE_FLOAT' => ['min_range', 'max_range'], + ]; + + $this->filterTypeOptions = []; + foreach ($map as $filter => $type) { + $constant = $this->getConstant($filter); + if ($constant === null) { + continue; + } + $this->filterTypeOptions[$constant] = $type; + } + + 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) { + return null; + } + + 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('FILTER_FLAG_ALLOW_OCTAL', $flagsType); + $allowHex = $this->hasFlag('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; + } + + /** + * @param non-empty-string $flagName + */ + private function hasFlag(string $flagName, ?Type $flagsType): bool + { + $flag = $this->getConstant($flagName); + if ($flag === null) { + return false; + } + + 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('FILTER_FLAG_STRIP_LOW', $flagsType) + || $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType) + || $this->hasFlag('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..d1679bcdda --- /dev/null +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -0,0 +1,40 @@ +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..d92e4e5882 --- /dev/null +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -0,0 +1,200 @@ +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 e768567ab1..1aeacdb0cf 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -2,105 +2,21 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; +use function count; +use function strtolower; -class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** - * All validation filters match 0x100. - */ - private const VALIDATION_FILTER_BITMASK = 0x100; - - private ReflectionProvider $reflectionProvider; - - private ConstantStringType $flagsString; - - /** @var array|null */ - private ?array $filterTypeMap = null; - - public function __construct(ReflectionProvider $reflectionProvider) - { - $this->reflectionProvider = $reflectionProvider; - - $this->flagsString = new ConstantStringType('flags'); - } - - /** - * @return array - */ - private function getFilterTypeMap(): array - { - if ($this->filterTypeMap !== null) { - return $this->filterTypeMap; - } - - $booleanType = new BooleanType(); - $floatType = new FloatType(); - $intType = new IntegerType(); - $stringType = new StringType(); - - $this->filterTypeMap = [ - $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, - $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, - $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, - $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, - $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, - $this->getConstant('FILTER_SANITIZE_URL') => $stringType, - $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, - $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, - $this->getConstant('FILTER_VALIDATE_EMAIL') => $stringType, - $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, - $this->getConstant('FILTER_VALIDATE_INT') => $intType, - $this->getConstant('FILTER_VALIDATE_IP') => $stringType, - $this->getConstant('FILTER_VALIDATE_MAC') => $stringType, - $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, - $this->getConstant('FILTER_VALIDATE_URL') => $stringType, - ]; - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; - } - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; - } - - return $this->filterTypeMap; - } - - private function getConstant(string $constantName): int + public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) { - $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); - } - - return $valueType->getValue(); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -108,144 +24,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'filter_var'; } - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope - ): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $mixedType = new MixedType(); - - $filterArg = $functionCall->getArgs()[1] ?? null; - if ($filterArg === null) { - $filterValue = $this->getConstant('FILTER_DEFAULT'); - } else { - $filterType = $scope->getType($filterArg->value); - if (!$filterType instanceof ConstantIntegerType) { - return $mixedType; - } - $filterValue = $filterType->getValue(); - } - - $flagsArg = $functionCall->getArgs()[2] ?? null; - $inputType = $scope->getType($functionCall->getArgs()[0]->value); - $exactType = $this->determineExactType($inputType, $filterValue); - if ($exactType !== null) { - $type = $exactType; - } else { - $type = $this->getFilterTypeMap()[$filterValue] ?? $mixedType; - $otherType = $this->getOtherType($flagsArg, $scope); - - if ($inputType->isNonEmptyString()->yes() - && $type instanceof StringType - && !$this->canStringBeSanitized($filterValue, $flagsArg, $scope)) { - $type = new IntersectionType([$type, new AccessoryNonEmptyStringType()]); - } - - if ($otherType->isSuperTypeOf($type)->no()) { - $type = new UnionType([$type, $otherType]); - } - } - - if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsArg, $scope)) { - return new ArrayType(new MixedType(), $type); - } - - return $type; - } - - - private function determineExactType(Type $in, int $filterValue): ?Type - { - if (($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN') && $in instanceof BooleanType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof IntegerType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof FloatType)) { - return $in; - } - - if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof IntegerType) { - return $in->toFloat(); - } - - return null; - } - - private function getOtherType(?Node\Arg $flagsArg, Scope $scope): Type - { - $falseType = new ConstantBooleanType(false); - if ($flagsArg === null) { - return $falseType; - } - - $defaultType = $this->getDefault($flagsArg, $scope); - if ($defaultType !== null) { - return $defaultType; - } - - if ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsArg, $scope)) { - return new NullType(); - } - - return $falseType; - } - - private function getDefault(Node\Arg $expression, Scope $scope): ?Type - { - $exprType = $scope->getType($expression->value); - if (!$exprType instanceof ConstantArrayType) { + if (count($functionCall->getArgs()) < 1) { return null; } - $optionsType = $exprType->getOffsetValueType(new ConstantStringType('options')); - if (!$optionsType instanceof ConstantArrayType) { - return null; - } - - $defaultType = $optionsType->getOffsetValueType(new ConstantStringType('default')); - if (!$defaultType instanceof ErrorType) { - return $defaultType; - } - - return null; - } - - - private function hasFlag(int $flag, ?Node\Arg $expression, Scope $scope): bool - { - if ($expression === null) { - return false; - } - - $type = $this->getFlagsValue($scope->getType($expression->value)); - - return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; - } - - private function getFlagsValue(Type $exprType): Type - { - if (!$exprType instanceof ConstantArrayType) { - return $exprType; - } - - return $exprType->getOffsetValueType($this->flagsString); - } - - private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool - { - // If it is a validation filter, the string will not be changed - if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { - return false; - } - - // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, - // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK - if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsArg, $scope); - } + $inputType = $scope->getType($functionCall->getArgs()[0]->value); + $filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; + $flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - return true; + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..330735d9e4 --- /dev/null +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -0,0 +1,64 @@ +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..e290bea849 100644 --- a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php @@ -2,15 +2,18 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class GetCalledClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,9 +23,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 ac2b38eb8e..14a06d90c0 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -4,23 +4,28 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ClassStringType; 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 +#[AutowiredService] +final class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -31,7 +36,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $args = $functionCall->getArgs(); + if (count($args) === 0) { + if ($scope->isInTrait()) { + return new ClassStringType(); + } + if ($scope->isInClass()) { return new ConstantStringType($scope->getClassReflection()->getName(), true); } @@ -41,6 +51,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,7 +62,12 @@ static function (Type $type, callable $traverse): Type { return $traverse($type); } - if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { + if ($type instanceof EnumCaseObjectType) { + return new GenericClassStringType(new ObjectType($type->getClassName())); + } + + $objectClassNames = $type->getObjectClassNames(); + if ($type instanceof TemplateType && $objectClassNames === []) { if ($type instanceof ObjectWithoutClassType) { return new GenericClassStringType($type); } @@ -64,14 +83,14 @@ static function (Type $type, callable $traverse): Type { ]); } elseif ($type instanceof StaticType) { return new GenericClassStringType($type->getStaticObjectType()); - } elseif ($type instanceof TypeWithClassName) { + } elseif ($objectClassNames !== []) { return new GenericClassStringType($type); } elseif ($type instanceof ObjectWithoutClassType) { return new ClassStringType(); } return new ConstantBooleanType(false); - } + }, ); } diff --git a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..6223240bb1 --- /dev/null +++ b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php @@ -0,0 +1,104 @@ +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..e401fd3f31 --- /dev/null +++ b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php @@ -0,0 +1,48 @@ +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 8dbcd52c3a..e4db21dda9 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -4,28 +4,31 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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,19 +37,16 @@ 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->getArgs()) === 0) { if ($scope->isInTrait()) { - return $defaultReturnType; + return null; } if ($scope->isInClass()) { return $this->findParentClassType( - $scope->getClassReflection() + $scope->getClassReflection(), ); } @@ -55,24 +55,20 @@ public function getTypeFromFunctionCall( $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { - return $defaultReturnType; + return null; } - $constantStrings = TypeUtils::getConstantStrings($argType); + $constantStrings = $argType->getConstantStrings(); if (count($constantStrings) > 0) { - return \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,11 +80,19 @@ 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(); diff --git a/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php b/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php deleted file mode 100644 index becf64a90e..0000000000 --- a/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php +++ /dev/null @@ -1,26 +0,0 @@ -getName() === 'getopt'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - return TypeUtils::toBenevolentUnion(ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType()); - } - -} diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index ac25fd63cd..757ffdf238 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -4,18 +4,20 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -43,8 +45,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $floatType; diff --git a/src/Type/Php/GettypeFunctionReturnTypeExtension.php b/src/Type/Php/GettypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..79c1c42124 --- /dev/null +++ b/src/Type/Php/GettypeFunctionReturnTypeExtension.php @@ -0,0 +1,92 @@ +getName() === 'gettype'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($functionCall->getArgs()[0]->value); + + return TypeTraverser::map($valueType, static function (Type $valueType, callable $traverse): Type { + if ($valueType instanceof UnionType || $valueType instanceof IntersectionType) { + return $traverse($valueType); + } + + if ($valueType->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($valueType->isArray()->yes()) { + return new ConstantStringType('array'); + } + + if ($valueType->isBoolean()->yes()) { + return new ConstantStringType('boolean'); + } + + $resource = new ResourceType(); + if ($resource->isSuperTypeOf($valueType)->yes()) { + return new UnionType([ + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + ]); + } + + if ($valueType->isInteger()->yes()) { + return new ConstantStringType('integer'); + } + + if ($valueType->isFloat()->yes()) { + // for historical reasons "double" is returned in case of a float, and not simply "float" + return new ConstantStringType('double'); + } + + if ($valueType->isNull()->yes()) { + return new ConstantStringType('NULL'); + } + + if ($valueType->isObject()->yes()) { + return new ConstantStringType('object'); + } + + return TypeCombinator::union( + new ConstantStringType('string'), + new ConstantStringType('array'), + new ConstantStringType('boolean'), + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + new ConstantStringType('integer'), + new ConstantStringType('double'), + new ConstantStringType('NULL'), + new ConstantStringType('object'), + new ConstantStringType('unknown type'), + ); + }); + } + +} diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index f977e240ad..7798d8f0e8 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -4,43 +4,156 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\MixedType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; +use function count; +use function hash_algos; +use function in_array; +use function is_bool; +use function strtolower; +#[AutowiredService] final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function isFunctionSupported(FunctionReflection $functionReflection): bool + private const SUPPORTED_FUNCTIONS = [ + 'hash' => [ + 'cryptographic' => false, + 'possiblyFalse' => false, + 'binary' => 2, + ], + 'hash_file' => [ + 'cryptographic' => false, + 'possiblyFalse' => true, + 'binary' => 2, + ], + 'hash_hkdf' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + 'binary' => true, + ], + 'hash_hmac' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + 'binary' => 3, + ], + 'hash_hmac_file' => [ + 'cryptographic' => true, + 'possiblyFalse' => true, + 'binary' => 3, + ], + 'hash_pbkdf2' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + 'binary' => 5, + ], + ]; + + 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) { - return $functionReflection->getName() === 'hash'; + $this->hashAlgorithms = hash_algos(); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $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 $defaultReturnType; + return null; } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - if ($argType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + if (is_bool($functionData['binary'])) { + $binaryType = new ConstantBooleanType($functionData['binary']); + } elseif (isset($functionCall->getArgs()[$functionData['binary']])) { + $binaryType = $scope->getType($functionCall->getArgs()[$functionData['binary']]->value); + } else { + $binaryType = new ConstantBooleanType(false); } - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + $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); } - $string = $values[0]; - return in_array($string->getValue(), hash_algos(), true) ? new StringType() : new ConstantBooleanType(false); + return $returnType; } } diff --git a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php b/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php deleted file mode 100644 index afc607f6e1..0000000000 --- a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php +++ /dev/null @@ -1,97 +0,0 @@ -getName(), ['hash_hmac', 'hash_hmac_file'], true); - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - if ($functionReflection->getName() === 'hash_hmac') { - $defaultReturnType = new StringType(); - } else { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; - } - - $argType = $scope->getType($functionCall->getArgs()[0]->value); - if ($argType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - $string = $values[0]; - - return in_array($string->getValue(), self::HMAC_ALGORITHMS, true) ? $defaultReturnType : new ConstantBooleanType(false); - } - -} diff --git a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..71f8e6643f --- /dev/null +++ b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php @@ -0,0 +1,53 @@ +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 0d390f7978..5b4a28b2ec 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -4,17 +4,21 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,7 +28,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], 2); + $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], [2], isList: TrinaryLogic::createYes()); $numberType = TypeUtils::toBenevolentUnion(TypeCombinator::union(new IntegerType(), new FloatType())); if (count($functionCall->getArgs()) < 1) { @@ -32,8 +36,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $numberType; diff --git a/src/Type/Php/IdateFunctionReturnTypeExtension.php b/src/Type/Php/IdateFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..456ff2b227 --- /dev/null +++ b/src/Type/Php/IdateFunctionReturnTypeExtension.php @@ -0,0 +1,42 @@ +getName() === 'idate'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if ($args === []) { + return new ConstantBooleanType(false); + } + + return $this->idateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($args[0]->value), + ); + } + +} diff --git a/src/Type/Php/IdateFunctionReturnTypeHelper.php b/src/Type/Php/IdateFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..c9c0f90c6e --- /dev/null +++ b/src/Type/Php/IdateFunctionReturnTypeHelper.php @@ -0,0 +1,77 @@ +getConstantStrings() as $formatString) { + $types[] = $this->buildReturnTypeFromFormat($formatString->getValue()); + } + + if ($types === []) { + return null; + } + + return TypeCombinator::union(...$types); + } + + public function buildReturnTypeFromFormat(string $formatString): Type + { + // see https://www.php.net/idate and https://www.php.net/manual/de/datetime.format + switch ($formatString) { + case 'd': + case 'j': + return IntegerRangeType::fromInterval(1, 31); + case 'h': + case 'g': + return IntegerRangeType::fromInterval(1, 12); + case 'H': + case 'G': + return IntegerRangeType::fromInterval(0, 23); + case 'i': + return IntegerRangeType::fromInterval(0, 59); + case 'I': + return IntegerRangeType::fromInterval(0, 1); + case 'L': + return IntegerRangeType::fromInterval(0, 1); + case 'm': + case 'n': + return IntegerRangeType::fromInterval(1, 12); + case 'N': + return IntegerRangeType::fromInterval(1, 7); + case 's': + return IntegerRangeType::fromInterval(0, 59); + case 't': + return IntegerRangeType::fromInterval(28, 31); + case 'w': + return IntegerRangeType::fromInterval(0, 6); + case 'W': + return IntegerRangeType::fromInterval(1, 53); + case 'y': + return IntegerRangeType::fromInterval(0, 99); + case 'z': + return IntegerRangeType::fromInterval(0, 365); + case 'B': + case 'o': + case 'U': + case 'Y': + case 'Z': + return new IntegerType(); + default: + return new ConstantBooleanType(false); + } + } + +} diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index ef18dc3272..699b21c09c 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -4,14 +4,28 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; +use function implode; +use function in_array; -class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -25,27 +39,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): Type { $args = $functionCall->getArgs(); if (count($args) === 1) { $argType = $scope->getType($args[0]->value); if ($argType->isArray()->yes()) { - $accessoryTypes = []; - if ($argType->isIterableAtLeastOnce()->yes() && $argType->getIterableValueType()->isNonEmptyString()->yes()) { - $accessoryTypes[] = new AccessoryNonEmptyStringType(); - } - if ($argType->getIterableValueType()->isLiteralString()->yes()) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } - - if (count($accessoryTypes) > 0) { - $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); - } - - return new StringType(); + return $this->implode($argType, new ConstantStringType('')); } } @@ -55,16 +56,52 @@ public function getTypeFromFunctionCall( $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 ($arrayType->getIterableValueType()->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + if ($valueTypeAsString->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($valueTypeAsString->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } + // implode is one of the four functions that can produce literal strings as blessed by the original RFC: wiki.php.net/rfc/is_literal if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } + if ($valueTypeAsString->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($valueTypeAsString->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); @@ -74,4 +111,38 @@ public function getTypeFromFunctionCall( 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 8e7fc5adfc..c26d550049 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -2,21 +2,31 @@ 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\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -31,30 +41,133 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->getArgs()) < 3) { + $argsCount = count($node->getArgs()); + if ($argsCount < 2) { return new SpecifiedTypes(); } - $strictNodeType = $scope->getType($node->getArgs()[2]->value); - if (!(new ConstantBooleanType(true))->isSuperTypeOf($strictNodeType)->yes()) { - return new SpecifiedTypes([], []); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $isStrictComparison = $strictNodeType->isTrue()->yes(); } - $arrayValueType = $scope->getType($node->getArgs()[1]->value)->getIterableValueType(); + $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->getArgs()[0]->value, + $specifiedTypes = $this->typeSpecifier->create( + $needleExpr, $arrayValueType, $context, - false, - $scope + $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..21aab0b82f --- /dev/null +++ b/src/Type/Php/IniGetReturnTypeExtension.php @@ -0,0 +1,66 @@ +getName() === 'ini_get'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + $types = [ + 'date.timezone' => new StringType(), + 'memory_limit' => new StringType(), + 'max_execution_time' => $numericString, + 'max_input_time' => $numericString, + 'default_socket_timeout' => $numericString, + 'precision' => $numericString, + ]; + + $argType = $scope->getType($args[0]->value); + $results = []; + foreach ($argType->getConstantStrings() as $constantString) { + if (!array_key_exists($constantString->getValue(), $types)) { + return null; + } + $results[] = $types[$constantString->getValue()]; + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index 0da850864a..baab09248b 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -2,17 +2,21 @@ namespace PHPStan\Type\Php; +use ArithmeticError; +use DivisionByZeroError; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionThrowTypeExtension; -use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function count; +use const PHP_INT_MIN; -class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension +#[AutowiredService] +final class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -26,40 +30,20 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $functionReflection->getThrowType(); } - $containsMin = false; - $valueType = $scope->getType($funcCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantScalars($valueType) as $constantScalarType) { - if ($constantScalarType->getValue() === PHP_INT_MIN) { - $containsMin = true; - } - - $valueType = TypeCombinator::remove($valueType, $constantScalarType); - } - - if (!$valueType instanceof NeverType) { - $containsMin = true; - } + $valueType = $scope->getType($funcCall->getArgs()[0]->value)->toInteger(); + $containsMin = $valueType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN)); - $divisionByZero = false; - $divisorType = $scope->getType($funcCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantScalars($divisorType) as $constantScalarType) { - if ($containsMin && $constantScalarType->getValue() === -1) { - return new ObjectType(\ArithmeticError::class); + $divisorType = $scope->getType($funcCall->getArgs()[1]->value)->toInteger(); + if (!$containsMin->no()) { + $divisionByMinusOne = $divisorType->isSuperTypeOf(new ConstantIntegerType(-1)); + if (!$divisionByMinusOne->no()) { + return new ObjectType(ArithmeticError::class); } - - if ($constantScalarType->getValue() === 0) { - $divisionByZero = true; - } - - $divisorType = TypeCombinator::remove($divisorType, $constantScalarType); - } - - if (!$divisorType instanceof NeverType) { - return new ObjectType($containsMin ? \ArithmeticError::class : \DivisionByZeroError::class); } - if ($divisionByZero) { - return new ObjectType(\DivisionByZeroError::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 92234ef118..5d19e4950f 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -2,78 +2,66 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; +use function count; +use function strtolower; -class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +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->getArgs()[0]) - && isset($node->getArgs()[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->getArgs()[1]->value; - $classNameArgExprType = $scope->getType($classNameArgExpr); - if ( - $classNameArgExpr instanceof ClassConstFetch - && $classNameArgExpr->class instanceof Name - && $classNameArgExpr->name instanceof \PhpParser\Node\Identifier - && strtolower($classNameArgExpr->name->name) === 'class' - ) { - $objectType = $scope->resolveTypeByName($classNameArgExpr->class); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof ConstantStringType) { - $objectType = new ObjectType($classNameArgExprType->getValue()); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof GenericClassStringType) { - $objectType = $classNameArgExprType->getGenericType(); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($context->true()) { - $objectType = new ObjectWithoutClassType(); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } else { - $types = new SpecifiedTypes(); + if (!$classType instanceof ConstantStringType && !$context->true()) { + return new SpecifiedTypes([], []); } - if (isset($node->getArgs()[2]) && $context->true()) { - if (!$scope->getType($node->getArgs()[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) { - $types = $types->intersectWith($this->typeSpecifier->create( - $node->getArgs()[0]->value, - isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(), - $context, - false, - $scope - )); - } + $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..df33262d8d --- /dev/null +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -0,0 +1,80 @@ +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 c324649f40..0beb301d46 100644 --- a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php @@ -8,15 +8,19 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -30,10 +34,10 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType(true), new MixedType(true)), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 50b77703e9..0000000000 --- a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,43 +0,0 @@ -getName()) === 'is_bool' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new BooleanType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index 16f3d55f8b..a0cde666e5 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -11,21 +11,22 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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 @@ -37,7 +38,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!isset($node->getArgs()[0])) { @@ -49,13 +50,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ( $value instanceof Array_ && count($value->items) === 2 - && $valueType instanceof ConstantArrayType + && $valueType->isConstantArray()->yes() && !$valueType->isCallable()->no() ) { - if ($value->items[0] === null || $value->items[1] === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - $functionCall = new FuncCall(new Name('method_exists'), [ new Arg($value->items[0]->value), new Arg($value->items[1]->value), @@ -63,7 +60,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $this->methodExistsExtension->specifyTypes($functionReflection, $functionCall, $scope, $context); } - return $this->typeSpecifier->create($value, new CallableType(), $context, false, $scope); + return $this->typeSpecifier->create($value, new CallableType(), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 864ef327f4..0000000000 --- a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,56 +0,0 @@ -getName()) === 'is_countable' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(\Countable::class), - ]), - $context, - false, - $scope - ); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 77ded103d8..0000000000 --- a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,47 +0,0 @@ -getName()), [ - 'is_float', - 'is_double', - 'is_real', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new FloatType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 27e6ea99cb..0000000000 --- a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,47 +0,0 @@ -getName()), [ - 'is_int', - 'is_integer', - 'is_long', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new IntegerType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php index 178ebf3bad..475f9a83cb 100644 --- a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php @@ -8,15 +8,19 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -27,14 +31,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new IterableType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new IterableType(new MixedType(), new MixedType()), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 7d1faec933..0000000000 --- a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,44 +0,0 @@ -getName()) === 'is_null' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new NullType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 5c4aaf6195..0000000000 --- a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,60 +0,0 @@ -getName() === 'is_numeric' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $numericTypes = [ - new IntegerType(), - new FloatType(), - ]; - - if ($context->truthy()) { - $numericTypes[] = new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType($numericTypes), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php deleted file mode 100644 index e48e70341d..0000000000 --- a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,44 +0,0 @@ -getName()) === 'is_object' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectWithoutClassType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 1dd6bc0354..0000000000 --- a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,44 +0,0 @@ -getName()) === 'is_resource' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ResourceType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 277262cd35..0000000000 --- a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,53 +0,0 @@ -getName() === 'is_scalar' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - new BooleanType(), - ]), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php deleted file mode 100644 index bb5832d5db..0000000000 --- a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,44 +0,0 @@ -getName()) === 'is_string' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new StringType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index 88416246c0..e910bd5ea1 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -8,28 +8,25 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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 { @@ -39,72 +36,32 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->getArgs()) < 2) { + if (!$context->true() || count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $objectType = $scope->getType($node->getArgs()[0]->value); + + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $classType = $scope->getType($node->getArgs()[1]->value); $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (!$classType instanceof ConstantStringType) { - if ($context->truthy()) { - if ($allowString) { - $type = TypeCombinator::union( - new ObjectWithoutClassType(), - new ClassStringType() - ); - } else { - $type = new ObjectWithoutClassType(); - } - - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - $type, - $context, - false, - $scope - ); - } - - 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->getArgs()[0]->value, - $type, + $resultType, $context, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..b4ed3b2d18 --- /dev/null +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -0,0 +1,61 @@ +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 c3cff87d86..3d320abd89 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,21 +2,27 @@ 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\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\TrinaryLogic; +use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +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 +#[AutowiredService] +final class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ @@ -25,79 +31,131 @@ 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(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + if ($functionReflection->getName() === 'json_decode') { + $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType); + } + if (!isset($functionCall->getArgs()[$argumentPosition])) { return $defaultReturnType; } $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; - $constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { - return $constrictedReturnType; + if ($functionReflection->getName() === 'json_encode' && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->yes()) { + return TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); } - $valueType = $scope->getType($optionsExpr); - if (!$valueType instanceof ConstantIntegerType) { - return $defaultReturnType; + return $defaultReturnType; + } + + private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type + { + $args = $funcCall->getArgs(); + $isForceArray = $this->isForceArray($funcCall, $scope); + if (!isset($args[0])) { + return $fallbackType; } - $value = $valueType->getValue(); - $throwOnErrorType = $this->reflectionProvider->getConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType(); - if (!$throwOnErrorType instanceof ConstantIntegerType) { - return $defaultReturnType; + $firstValueType = $scope->getType($args[0]->value); + if ($firstValueType->getConstantStrings() !== []) { + $types = []; + + foreach ($firstValueType->getConstantStrings() as $constantString) { + $types[] = $this->resolveConstantStringType($constantString, $isForceArray); + } + + return TypeCombinator::union(...$types); } - $throwOnErrorValue = $throwOnErrorType->getValue(); - if (($value & $throwOnErrorValue) !== $throwOnErrorValue) { - return $defaultReturnType; + if ($isForceArray->yes()) { + return TypeCombinator::remove($fallbackType, new ObjectWithoutClassType()); } - return $constrictedReturnType; + return $fallbackType; } - private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool + /** + * Is "json_decode(..., true)"? + */ + private function isForceArray(FuncCall $funcCall, Scope $scope): TrinaryLogic { - if ($expr instanceof ConstFetch) { - $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); - if ($constant === 'JSON_THROW_ON_ERROR') { - return true; + $args = $funcCall->getArgs(); + $flagValue = $this->getFlagValue($funcCall, $scope); + if (!isset($args[1])) { + return TrinaryLogic::createNo(); + } + + $secondArgType = $scope->getType($args[1]->value); + $secondArgValues = []; + foreach ($secondArgType->getConstantScalarValues() as $value) { + if ($value === null) { + $secondArgValues[] = $flagValue; + continue; + } + if (!is_bool($value)) { + return TrinaryLogic::createNo(); } + $secondArgValues[] = TrinaryLogic::createFromBoolean($value); + } + + if ($secondArgValues === []) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::extremeIdentity(...$secondArgValues); + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, TrinaryLogic $isForceArray): Type + { + $types = []; + /** @var bool $asArray */ + foreach ($isForceArray->toBooleanType()->getConstantScalarValues() as $asArray) { + $decodedValue = json_decode($constantStringType->getValue(), $asArray); + $types[] = ConstantTypeHelper::getTypeFromValue($decodedValue); } - if (!$expr instanceof BitwiseOr) { - return false; + return TypeCombinator::union(...$types); + } + + private function getFlagValue(FuncCall $funcCall, Scope $scope): TrinaryLogic + { + $args = $funcCall->getArgs(); + if (!isset($args[3])) { + return TrinaryLogic::createNo(); } - return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); + // depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array + return $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($args[3]->value, $scope, 'JSON_OBJECT_AS_ARRAY'); } } diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index a348c3b5bb..19cfcbaaea 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -2,99 +2,65 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\BitwiseOr; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function in_array; -class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension +#[AutowiredService] +final class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - /** @var array */ - private array $argumentPositions = [ + private const ARGUMENTS_POSITIONS = [ 'json_encode' => 1, '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 Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( + return in_array( $functionReflection->getName(), [ 'json_encode', 'json_decode', ], - true - ); + true, + ) && $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getThrowTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): ?Type { - $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; + $argumentPosition = self::ARGUMENTS_POSITIONS[$functionReflection->getName()]; if (!isset($functionCall->getArgs()[$argumentPosition])) { return null; } $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { + if (!$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->no()) { return new ObjectType('JsonException'); } - $valueType = $scope->getType($optionsExpr); - if (!$valueType instanceof ConstantIntegerType) { - return null; - } - - $value = $valueType->getValue(); - $throwOnErrorType = $this->reflectionProvider->getConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType(); - if (!$throwOnErrorType instanceof ConstantIntegerType) { - return null; - } - - $throwOnErrorValue = $throwOnErrorType->getValue(); - if (($value & $throwOnErrorValue) !== $throwOnErrorValue) { - return null; - } - - return new ObjectType('JsonException'); - } - - private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool - { - if ($expr instanceof ConstFetch) { - $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); - if ($constant === 'JSON_THROW_ON_ERROR') { - return true; - } - } - - if (!$expr instanceof BitwiseOr) { - return false; - } - - return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); + return null; } } diff --git a/src/Type/Php/LtrimFunctionReturnTypeExtension.php b/src/Type/Php/LtrimFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..2db0c7be4e --- /dev/null +++ b/src/Type/Php/LtrimFunctionReturnTypeExtension.php @@ -0,0 +1,87 @@ +getName() === 'ltrim'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $string = $scope->getType($functionCall->getArgs()[0]->value); + + $accessory = []; + $defaultType = new StringType(); + if ($string->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($string->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $defaultType = new IntersectionType($accessory); + } + + if (count($functionCall->getArgs()) !== 2) { + return $defaultType; + } + + $trimChars = $scope->getType($functionCall->getArgs()[1]->value); + + $trimConstantStrings = $trimChars->getConstantStrings(); + if (count($trimConstantStrings) > 0) { + $result = []; + $stringConstantStrings = $string->getConstantStrings(); + + foreach ($trimConstantStrings as $trimConstantString) { + if (count($stringConstantStrings) > 0) { + foreach ($stringConstantStrings as $stringConstantString) { + $result[] = new ConstantStringType( + ltrim($stringConstantString->getValue(), $trimConstantString->getValue()), + true, + ); + } + } elseif ($trimConstantString->getValue() === '\\' && $string->isClassString()->yes()) { + $result[] = new ClassStringType(); + } elseif (preg_match('/\d/', $trimConstantString->getValue()) === 0 && $string->isNumericString()->yes()) { + $result[] = new AccessoryNumericStringType(); + } else { + return $defaultType; + } + } + + return TypeCombinator::union(...$result); + } + + return $defaultType; + } + +} diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 7fa2af76d0..f12efd095c 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -4,18 +4,33 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function count; +use function str_contains; +use function strtolower; +use function trim; -class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_convert_encoding'; @@ -24,25 +39,104 @@ 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->getArgs()[0])) { - return $defaultReturnType; + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = (new StringType())->isSuperTypeOf($argType); - $isArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType); - $compare = $isString->compareTo($isArray); - if ($compare === $isString) { + + $initialReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + $result = TypeCombinator::intersect($initialReturnType, $this->generalizeStringType($argType)); + if ($result instanceof NeverType) { + $result = $initialReturnType; + } + + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!isset($functionCall->getArgs()[2])) { + return TypeCombinator::remove($result, new ConstantBooleanType(false)); + } + $fromEncodingArgType = $scope->getType($functionCall->getArgs()[2]->value); + + $returnFalseIfCannotDetectEncoding = false; + if (!$fromEncodingArgType->isArray()->no()) { + $constantArrays = $fromEncodingArgType->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + if (count($constantArray->getValueTypes()) > 1) { + $returnFalseIfCannotDetectEncoding = true; + break; + } + } + } else { + $returnFalseIfCannotDetectEncoding = true; + } + } + if (!$returnFalseIfCannotDetectEncoding && !$fromEncodingArgType->isString()->no()) { + $constantStrings = $fromEncodingArgType->getConstantStrings(); + if (count($constantStrings) > 0) { + foreach ($constantStrings as $constantString) { + if ( + str_contains($constantString->getValue(), ',') + || trim(strtolower($constantString->getValue())) === 'auto' + ) { + $returnFalseIfCannotDetectEncoding = true; + break; + } + } + } else { + $returnFalseIfCannotDetectEncoding = true; + } + } + + if (!$returnFalseIfCannotDetectEncoding) { + return TypeCombinator::remove($result, new ConstantBooleanType(false)); + } + } + + return TypeCombinator::union($result, new ConstantBooleanType(false)); + } + + public function generalizeStringType(Type $type): Type + { + if ($type instanceof UnionType) { + return $type->traverse([$this, 'generalizeStringType']); + } + + if ($type->isString()->yes()) { return new StringType(); - } elseif ($compare === $isArray) { - return new ArrayType(new IntegerType(), new StringType()); } - return $defaultReturnType; + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $types = []; + foreach ($constantArrays as $constantArray) { + $types[] = $constantArray->traverse([$this, 'generalizeStringType']); + } + + return TypeCombinator::union(...$types); + } + + if ($type->isArray()->yes()) { + $newArrayType = new ArrayType($type->getIterableKeyType(), $this->generalizeStringType($type->getIterableValueType())); + if ($type->isIterableAtLeastOnce()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + if ($type->isList()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType()); + } + + return $newArrayType; + } + + return $type; } } diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index f2ce073b64..596e2ed8c1 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -4,22 +4,29 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $supportedEncodings; + use MbFunctionsReturnTypeExtensionTrait; /** @var int[] */ private array $encodingPositionMap = [ @@ -27,24 +34,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 +47,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->getArgs()) < $positionEncodingParam) { return TypeCombinator::remove($returnType, new BooleanType()); } - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[$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..48d6b5e712 --- /dev/null +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -0,0 +1,153 @@ +getName() === 'mb_strlen'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $encodings = []; + + if (count($functionCall->getArgs()) === 1) { + // there is a chance to get an unsupported encoding 'pass' or 'none' here on PHP 7.3-7.4 + $encodings = [mb_internal_encoding()]; + } elseif (count($functionCall->getArgs()) === 2) { // custom encoding is specified + $encodings = array_map( + static fn (ConstantStringType $t) => $t->getValue(), + $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(), + ); + } + + if (count($encodings) > 0) { + for ($i = 0; $i < count($encodings); $i++) { + if ($this->isSupportedEncoding($encodings[$i])) { + continue; + } + $encodings[$i] = self::UNSUPPORTED_ENCODING; + } + + $encodings = array_unique($encodings); + + if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + return new NeverType(); + } + return new ConstantBooleanType(false); + } + } else { // if there aren't encoding constants, use all available encodings + $encodings = array_merge($this->getSupportedEncodings(), [self::UNSUPPORTED_ENCODING]); + } + + $argType = $scope->getType($args[0]->value); + $constantScalars = $argType->getConstantScalarValues(); + + $lengths = []; + foreach ($constantScalars as $constantScalar) { + $stringScalar = (string) $constantScalar; + + foreach ($encodings as $encoding) { + if (!$this->isSupportedEncoding($encoding)) { + continue; + } + + $length = @mb_strlen($stringScalar, $encoding); + if ($length === false) { + throw new ShouldNotHappenException(sprintf('Got false on a supported encoding %s and value %s', $encoding, var_export($stringScalar, true))); + } + $lengths[] = $length; + } + } + + $isNonEmpty = $argType->isNonEmptyString(); + $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( + $isNonEmpty->yes() + || $numeric->isSuperTypeOf($argType)->yes() + || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() + ) { + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); + } else { + $range = TypeCombinator::remove( + ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(), + new ConstantBooleanType(false), + ); + } + + if (!$this->phpVersion->throwsOnInvalidMbStringEncoding() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { + return TypeCombinator::union($range, new ConstantBooleanType(false)); + } + return $range; + } + +} diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index f1aeb5139f..7b14c0cce7 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; @@ -12,21 +13,18 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; +use function strtolower; -class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -53,14 +51,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, new ConstantStringType('none'), new ConstantStringType('long'), new ConstantStringType('entity'), - ...$ranges + ...$ranges, ); } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = (new StringType())->isSuperTypeOf($argType); - $isNull = (new NullType())->isSuperTypeOf($argType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType); + $isString = $argType->isString(); + $isNull = $argType->isNull(); + $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { @@ -107,7 +105,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($argType instanceof ConstantStringType) { $value = strtolower($argType->getValue()); - if ($value === 'none' || $value === 'long' || $value === 'entity') { + if (in_array($value, ['none', 'long', 'entity'], true)) { return new ConstantBooleanType(true); } diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 16535fcbb6..3db54618e5 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -3,23 +3,26 @@ 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; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -32,11 +35,11 @@ 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() + && $context->true() && count($node->getArgs()) >= 2; } @@ -44,18 +47,33 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { + $methodNameType = $scope->getType($node->getArgs()[1]->value); + if (!$methodNameType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + $objectType = $scope->getType($node->getArgs()[0]->value); - if (!$objectType instanceof ObjectType) { - if ((new StringType())->isSuperTypeOf($objectType)->yes()) { - return new SpecifiedTypes([], []); + if ($objectType->isString()->yes()) { + if ($objectType->isClassString()->yes()) { + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new IntersectionType([ + $objectType, + new HasMethodType($methodNameType->getValue()), + ]), + $context, + $scope, + ); } - } - $methodNameType = $scope->getType($node->getArgs()[1]->value); - if (!$methodNameType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } @@ -69,8 +87,7 @@ public function specifyTypes( new ClassStringType(), ]), $context, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index f96fb94c68..63ab8b2669 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -4,16 +4,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -28,8 +31,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new FloatType(); diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index 41603eb52a..26864a3054 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -6,60 +6,48 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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 +#[AutowiredService] +final class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $functionNames = [ - 'min' => '', - 'max' => '', - ]; + public function __construct( + private PhpVersion $phpVersion, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return isset($this->functionNames[$functionReflection->getName()]); + return in_array($functionReflection->getName(), ['min', 'max'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) === 1) { $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no()) { - return new ConstantBooleanType(false); - } - $iterableValueType = $argType->getIterableValueType(); - $argumentTypes = []; - if (!$isIterable->yes()) { - $argumentTypes[] = new ConstantBooleanType(false); - } - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - - return $this->processType( + return $this->processArrayType( $functionReflection->getName(), - $argumentTypes + $argType, ); } @@ -75,17 +63,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType1 = $scope->getType($args[1]->value); if ($argType0->isArray()->no() && $argType1->isArray()->no()) { + $comparisonExpr = new Smaller( + new AlwaysRememberedExpr($args[0]->value, $argType0, $scope->getNativeType($args[0]->value)), + new AlwaysRememberedExpr($args[1]->value, $argType1, $scope->getNativeType($args[1]->value)), + ); + if ($functionName === 'min') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[0]->value, - $args[1]->value + $args[1]->value, )); } elseif ($functionName === 'max') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[1]->value, - $args[0]->value + $args[0]->value, )); } } @@ -111,32 +104,71 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->processType( $functionName, - $argumentTypes + $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; @@ -157,19 +189,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; } @@ -178,9 +210,9 @@ private function compareTypes( $firstType instanceof ConstantArrayType && $secondType instanceof ConstantArrayType ) { - if ($secondType->count() < $firstType->count()) { + if ($secondType->getArraySize() < $firstType->getArraySize()) { return $secondType; - } elseif ($firstType->count() < $secondType->count()) { + } elseif ($firstType->getArraySize() < $secondType->getArraySize()) { return $firstType; } diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index 4a484c150d..913f97a10c 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -2,16 +2,24 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use function count; +use function in_array; +use const ENT_SUBSTITUTE; -class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,35 +29,43 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'addcslashes', 'escapeshellarg', 'escapeshellcmd', - 'strtoupper', - 'strtolower', - 'mb_strtoupper', - 'mb_strtolower', - 'lcfirst', - 'ucfirst', - 'ucwords', 'htmlspecialchars', 'htmlentities', 'urlencode', 'urldecode', + 'preg_quote', 'rawurlencode', 'rawurldecode', - 'vsprintf', ], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; + } + + if (in_array($functionReflection->getName(), [ + 'htmlspecialchars', + 'htmlentities', + ], true)) { + if (!$this->isSubstituteFlagSet($args, $scope)) { + return new StringType(); + } } $argType = $scope->getType($args[0]->value); + if ($argType->isNonFalsyString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } if ($argType->isNonEmptyString()->yes()) { return new IntersectionType([ new StringType(), @@ -60,4 +76,22 @@ public function getTypeFromFunctionCall( return new StringType(); } + /** + * @param Arg[] $args + */ + private function isSubstituteFlagSet( + array $args, + Scope $scope, + ): bool + { + if (!isset($args[1])) { + return true; + } + $flagsType = $scope->getType($args[1]->value); + if (!$flagsType instanceof ConstantIntegerType) { + return false; + } + return (bool) ($flagsType->getValue() & ENT_SUBSTITUTE); + } + } diff --git a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php index 5dd2c9a723..8da8eda986 100644 --- a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php @@ -4,15 +4,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use function in_array; -final class NumberFormatFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class NumberFormatFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php b/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php new file mode 100644 index 0000000000..6784622a2a --- /dev/null +++ b/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php @@ -0,0 +1,72 @@ +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/OpensslCipherFunctionsReturnTypeExtension.php b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php new file mode 100644 index 0000000000..813ba532d2 --- /dev/null +++ b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php @@ -0,0 +1,90 @@ +getName(), ['openssl_cipher_iv_length', 'openssl_cipher_key_length'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return null; + } + + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $strings = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $results = array_unique(array_map(fn (ConstantStringType $algorithm): bool => $this->isSupportedAlgorithm($algorithm->getValue()), $strings)); + + if (count($results) !== 1) { + return null; + } + + $returnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + return $results[0] + ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) + : new ConstantBooleanType(false); + } + + private function isSupportedAlgorithm(string $algorithm): bool + { + return in_array(strtoupper($algorithm), $this->getSupportedAlgorithms(), true); + } + + /** @return string[] */ + private function getSupportedAlgorithms(): array + { + if (!is_null($this->supportedAlgorithms)) { + return $this->supportedAlgorithms; + } + + $supportedAlgorithms = []; + if (function_exists('openssl_get_cipher_methods')) { + $supportedAlgorithms = openssl_get_cipher_methods(true); + } + $this->supportedAlgorithms = array_map('strtoupper', $supportedAlgorithms); + + return $this->supportedAlgorithms; + } + +} diff --git a/src/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php new file mode 100644 index 0000000000..9aa71ccd27 --- /dev/null +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -0,0 +1,78 @@ +phpVersion->hasPDOSubclasses() && $methodReflection->getName() === 'connect'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $subclasses = []; + foreach ($constantStrings as $constantString) { + if (str_starts_with($constantString->getValue(), 'mysql:')) { + $subclasses['PDO\Mysql'] = 'PDO\Mysql'; + } elseif (str_starts_with($constantString->getValue(), 'firebird:')) { + $subclasses['PDO\Firebird'] = 'PDO\Firebird'; + } elseif (str_starts_with($constantString->getValue(), 'dblib:')) { + $subclasses['PDO\Dblib'] = 'PDO\Dblib'; + } elseif (str_starts_with($constantString->getValue(), 'odbc:')) { + $subclasses['PDO\Odbc'] = 'PDO\Odbc'; + } elseif (str_starts_with($constantString->getValue(), 'pgsql:')) { + $subclasses['PDO\Pgsql'] = 'PDO\Pgsql'; + } elseif (str_starts_with($constantString->getValue(), 'sqlite:')) { + $subclasses['PDO\Sqlite'] = 'PDO\Sqlite'; + } else { + return null; + } + } + + $returnTypes = []; + foreach ($subclasses as $class) { + $returnTypes[] = new ObjectType($class); + } + + return TypeCombinator::union(...$returnTypes); + } + +} diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php new file mode 100644 index 0000000000..8e4a2de7d1 --- /dev/null +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -0,0 +1,62 @@ +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 47d276e872..66035119d3 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -4,20 +4,34 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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; + +#[AutowiredService] final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -27,19 +41,25 @@ final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctio /** @var array|null */ private ?array $componentTypesPairedStrings = null; + /** @var array|null */ + private ?array $componentTypesPairedConstantsForLowercaseString = null; + + /** @var array|null */ + private ?array $componentTypesPairedStringsForLowercaseString = null; + private ?Type $allComponentsTogetherType = null; + private ?Type $allComponentsTogetherTypeForLowercaseString = null; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'parse_url'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants() - )->getReturnType(); + return null; } $this->cacheReturnTypes(); @@ -48,59 +68,104 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($functionCall->getArgs()) > 1) { $componentType = $scope->getType($functionCall->getArgs()[1]->value); - if (!$componentType instanceof ConstantType) { - return $this->createAllComponentsReturnType(); + if (!$componentType->isConstantValue()->yes()) { + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); - if (!$componentType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); } - if ($urlType instanceof ConstantStringType) { - try { - $result = @parse_url($urlType->getValue(), $componentType->getValue()); - } catch (\ValueError $e) { - return new ConstantBooleanType(false); + if (count($urlType->getConstantStrings()) > 0) { + $types = []; + foreach ($urlType->getConstantStrings() as $constantString) { + try { + $result = @parse_url($constantString->getValue(), $componentType->getValue()); + } catch (ValueError) { + $types[] = new ConstantBooleanType(false); + continue; + } + + $types[] = $scope->getTypeFromValue($result); } - return $scope->getTypeFromValue($result); + return TypeCombinator::union(...$types); } if ($componentType->getValue() === -1) { - 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 @@ -110,34 +175,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 b382c4ce7c..49cb0dffdb 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,30 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'pathinfo'; @@ -21,28 +33,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->getArgs()); if ($argsCount === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } elseif ($argsCount === 1) { - $stringType = new StringType(); - - $builder = ConstantArrayTypeBuilder::createFromConstantArray( - new ConstantArrayType( - [new ConstantStringType('dirname'), new ConstantStringType('basename'), new ConstantStringType('filename')], - [$stringType, $stringType, $stringType] - ) - ); - $builder->setOffsetValueType(new ConstantStringType('extension'), $stringType, true); - - return $builder->getArray(); + return null; + } + + $pathType = $scope->getType($functionCall->getArgs()[0]->value); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes()); + $builder->setOffsetValueType(new ConstantStringType('basename'), new StringType()); + $builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true); + $builder->setOffsetValueType(new ConstantStringType('filename'), new StringType()); + $arrayType = $builder->getArray(); + + if ($argsCount === 1) { + return $arrayType; + } + + $flagsType = $scope->getType($functionCall->getArgs()[1]->value); + + $scalarValues = $flagsType->getConstantScalarValues(); + if ($scalarValues !== []) { + $pathInfoAll = $this->getConstant('PATHINFO_ALL'); + if ($pathInfoAll === null) { + return null; + } + + $result = []; + foreach ($scalarValues as $scalarValue) { + if ($scalarValue === $pathInfoAll) { + $result[] = $arrayType; + } else { + $result[] = new StringType(); + } + } + + return TypeCombinator::union(...$result); + } + + return TypeCombinator::union($arrayType, new StringType()); + } + + /** + * @param non-empty-string $constantName + */ + private function getConstant(string $constantName): ?int + { + if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) { + return null; + } + + $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); } - return new StringType(); + return $valueType->getValue(); } } diff --git a/src/Type/Php/PowFunctionReturnTypeExtension.php b/src/Type/Php/PowFunctionReturnTypeExtension.php index e8dc55f83f..d2c0ba4830 100644 --- a/src/Type/Php/PowFunctionReturnTypeExtension.php +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -2,19 +2,17 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Pow; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; +use function count; -class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,31 +20,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'pow'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($firstArgType instanceof MixedType || $secondArgType instanceof MixedType) { - return $defaultReturnType; - } - - $object = new ObjectWithoutClassType(); - if ( - !$object->isSuperTypeOf($firstArgType)->no() - || !$object->isSuperTypeOf($secondArgType)->no() - ) { - return TypeCombinator::union($firstArgType, $secondArgType); - } - - return $defaultReturnType; + return $scope->getType(new Pow($functionCall->getArgs()[0]->value, $functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..6d6660d362 --- /dev/null +++ b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php @@ -0,0 +1,53 @@ +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..b8bd415b45 --- /dev/null +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,57 @@ +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..399ee9126f --- /dev/null +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,94 @@ +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 ($context->true() && $context->falsey()) { + $wasMatched = TrinaryLogic::createMaybe(); + } elseif ($context->true()) { + $wasMatched = TrinaryLogic::createYes(); + } else { + $wasMatched = TrinaryLogic::createNo(); + } + + if ($functionReflection->getName() === 'preg_match') { + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, $wasMatched, $scope); + } else { + $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, $wasMatched, $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..1be24ff645 --- /dev/null +++ b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php @@ -0,0 +1,62 @@ +getName() === 'preg_replace_callback' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + $patternArg = $args[0] ?? null; + $flagsArg = $args[5] ?? null; + + if ( + $patternArg === null + ) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchesType === null) { + return null; + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new StringType(), + ); + } + +} diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 83e1ca6aa9..ec1c814a47 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -2,76 +2,212 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Arg; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\BitwiseFlagHelper; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; - -class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +use PHPStan\Type\UnionType; +use function count; +use function is_array; +use function is_int; +use function is_numeric; +use function preg_split; +use function strtolower; + +#[AutowiredService] +final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private readonly BitwiseFlagHelper $bitwiseFlagAnalyser, + ) { - $this->reflectionProvider = $reflectionProvider; } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'preg_split'; } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $flagsArg = $functionCall->getArgs()[3] ?? null; + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + $patternArg = $args[0]; + $subjectArg = $args[1]; + $limitArg = $args[2] ?? null; + $capturesOffset = $args[3] ?? null; + $patternType = $scope->getType($patternArg->value); + $patternConstantTypes = $patternType->getConstantStrings(); + if (count($patternConstantTypes) > 0) { + foreach ($patternConstantTypes as $patternConstantType) { + if ($this->isValidPattern($patternConstantType->getValue()) === false) { + return new ErrorType(); + } + } + } + + $subjectType = $scope->getType($subjectArg->value); + $subjectConstantTypes = $subjectType->getConstantStrings(); + + $limits = []; + if ($limitArg === null) { + $limits = [-1]; + } else { + $limitType = $scope->getType($limitArg->value); + if (!$this->isIntOrStringValue($limitType)) { + return new ErrorType(); + } + foreach ($limitType->getConstantScalarValues() as $limit) { + if (!is_int($limit) && !is_numeric($limit)) { + return new ErrorType(); + } + $limits[] = $limit; + } + } + + $flags = []; + if ($capturesOffset === null) { + $flags = [0]; + } else { + $flagType = $scope->getType($capturesOffset->value); + if (!$this->isIntOrStringValue($flagType)) { + return new ErrorType(); + } + foreach ($flagType->getConstantScalarValues() as $flag) { + if (!is_int($flag) && !is_numeric($flag)) { + return new ErrorType(); + } + $flags[] = $flag; + } + } - if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) { - $type = new ArrayType( - new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), new IntegerType()]) + if ($this->isPatternOrSubjectEmpty($patternConstantTypes, $subjectConstantTypes)) { + if ($capturesOffset !== null + && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($capturesOffset->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes()) { + $returnStringType = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + } else { + $returnStringType = new StringType(); + } + + $arrayTypeBuilder = ConstantArrayTypeBuilder::createEmpty(); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(0), + $returnStringType, + ); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(1), + IntegerRangeType::fromInterval(0, null), ); - return TypeCombinator::union($type, new ConstantBooleanType(false)); + $capturedArrayType = $arrayTypeBuilder->getArray(); + + $returnInternalValueType = $returnStringType; + if ($capturesOffset !== null) { + $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($capturesOffset->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); + if ($flagState->yes()) { + $capturedArrayListType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), $capturedArrayType), + new AccessoryArrayListType(), + ); + + if ($subjectType->isNonEmptyString()->yes()) { + $capturedArrayListType = TypeCombinator::intersect($capturedArrayListType, new NonEmptyArrayType()); + } + return TypeCombinator::union($capturedArrayListType, new ConstantBooleanType(false)); + } + if ($flagState->maybe()) { + $returnInternalValueType = TypeCombinator::union(new StringType(), $capturedArrayType); + } + } + + $returnListType = TypeCombinator::intersect(new ArrayType(new MixedType(), $returnInternalValueType), new AccessoryArrayListType()); + if ($subjectType->isNonEmptyString()->yes()) { + $returnListType = TypeCombinator::intersect( + $returnListType, + new NonEmptyArrayType(), + ); + } + + return TypeCombinator::union($returnListType, new ConstantBooleanType(false)); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $resultTypes = []; + foreach ($patternConstantTypes as $patternConstantType) { + foreach ($subjectConstantTypes as $subjectConstantType) { + foreach ($limits as $limit) { + foreach ($flags as $flag) { + $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int) $limit, (int) $flag); + if ($result === false) { + return new ErrorType(); + } + $constantArray = ConstantArrayTypeBuilder::createEmpty(); + foreach ($result as $key => $value) { + if (is_array($value)) { + $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(1), new ConstantIntegerType($value[1])); + $returnInternalValueType = $valueConstantArray->getArray(); + } else { + $returnInternalValueType = new ConstantStringType($value); + } + $constantArray->setOffsetValueType(new ConstantIntegerType($key), $returnInternalValueType); + } + + $resultTypes[] = $constantArray->getArray(); + } + } + } + } + $resultTypes[] = new ConstantBooleanType(false); + return TypeCombinator::union(...$resultTypes); } + /** + * @param ConstantStringType[] $patternConstantArray + * @param ConstantStringType[] $subjectConstantArray + */ + private function isPatternOrSubjectEmpty(array $patternConstantArray, array $subjectConstantArray): bool + { + return count($patternConstantArray) === 0 || count($subjectConstantArray) === 0; + } - private function hasFlag(int $flag, ?Arg $expression, Scope $scope): bool + private function isValidPattern(string $pattern): bool { - if ($expression === null) { + try { + Strings::match('', $pattern); + } catch (RegexpException) { return false; } - - $type = $scope->getType($expression->value); - return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + return true; } - - private function getConstant(string $constantName): int + private function isIntOrStringValue(Type $type): bool { - $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); - } - - return $valueType->getValue(); + return (new UnionType([new IntegerType(), new StringType()]))->isSuperTypeOf($type)->yes(); } } diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index bc970ee7c7..e62a04d409 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -5,29 +5,31 @@ 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; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +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,11 +40,11 @@ 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() + && $context->true() && count($node->getArgs()) >= 2; } @@ -50,21 +52,30 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { $propertyNameType = $scope->getType($node->getArgs()[1]->value); if (!$propertyNameType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + + if ($propertyNameType->getValue() === '') { return new SpecifiedTypes([], []); } $objectType = $scope->getType($node->getArgs()[0]->value); if ($objectType instanceof ConstantStringType) { return new SpecifiedTypes([], []); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($objectType)->yes()) { + } elseif ($objectType->isObject()->yes()) { $propertyNode = new PropertyFetch( $node->getArgs()[0]->value, - new Identifier($propertyNameType->getValue()) + new Identifier($propertyNameType->getValue()), ); } else { return new SpecifiedTypes([], []); @@ -84,8 +95,7 @@ public function specifyTypes( new HasPropertyType($propertyNameType->getValue()), ]), $context, - false, - $scope + $scope, ); } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index 53243678ad..35f978e631 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -4,29 +4,37 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return in_array($functionReflection->getName(), ['random_int', 'rand'], true); + return in_array($functionReflection->getName(), ['random_int', 'rand', 'mt_rand'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if ($functionReflection->getName() === 'rand' && count($functionCall->getArgs()) === 0) { + if (in_array($functionReflection->getName(), ['rand', 'mt_rand'], true) && count($functionCall->getArgs()) === 0) { return IntegerRangeType::fromInterval(0, null); } if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + return null; } $minType = $scope->getType($functionCall->getArgs()[0]->value)->toInteger(); @@ -47,7 +55,7 @@ static function (Type $type): ?int { } return null; }, - $minType instanceof UnionType ? $minType->getTypes() : [$minType] + $minType instanceof UnionType ? $minType->getTypes() : [$minType], ); $maxValues = array_map( @@ -60,7 +68,7 @@ static function (Type $type): ?int { } return null; }, - $maxType instanceof UnionType ? $maxType->getTypes() : [$maxType] + $maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], ); assert(count($minValues) > 0); @@ -68,7 +76,7 @@ static function (Type $type): ?int { return IntegerRangeType::fromInterval( in_array(null, $minValues, true) ? null : min($minValues), - in_array(null, $maxValues, true) ? null : max($maxValues) + in_array(null, $maxValues, true) ? null : max($maxValues), ); } diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 12a72bce8a..de6e43bd79 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -4,8 +4,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -13,17 +14,22 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; 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 +#[AutowiredService] +final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const RANGE_LENGTH_THRESHOLD = 50; @@ -33,10 +39,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'range'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $startType = $scope->getType($functionCall->getArgs()[0]->value); @@ -45,36 +51,79 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantReturnTypes = []; - $startConstants = TypeUtils::getConstantScalars($startType); + $startConstants = $startType->getConstantScalarTypes(); foreach ($startConstants as $startConstant) { if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType && !$startConstant instanceof ConstantStringType) { continue; } - $endConstants = TypeUtils::getConstantScalars($endType); + $endConstants = $endType->getConstantScalarTypes(); foreach ($endConstants as $endConstant) { if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType && !$endConstant instanceof ConstantStringType) { continue; } - $stepConstants = TypeUtils::getConstantScalars($stepType); + $stepConstants = $stepType->getConstantScalarTypes(); foreach ($stepConstants as $stepConstant) { if (!$stepConstant instanceof ConstantIntegerType && !$stepConstant instanceof ConstantFloatType) { continue; } - $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + try { + $rangeValues = @range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + } catch (ValueError) { + continue; + } + + // @phpstan-ignore function.alreadyNarrowedType + if (!is_array($rangeValues)) { + continue; + } + if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { - return new IntersectionType([ + 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()) - ) + $endConstant->generalize(GeneralizePrecision::moreSpecific()), + $stepType->generalize(GeneralizePrecision::moreSpecific()), + ), ), new NonEmptyArrayType(), - ]); + new AccessoryArrayListType(), + ); } $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($rangeValues as $value) { @@ -91,31 +140,35 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = TypeCombinator::union($startType, $endType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType)->yes(); - $isStepInteger = (new IntegerType())->isSuperTypeOf($stepType)->yes(); + $isInteger = $argType->isInteger()->yes(); + $isStepInteger = $stepType->isInteger()->yes(); if ($isInteger && $isStepInteger) { - return new ArrayType(new IntegerType(), new IntegerType()); + if ($argType instanceof IntegerRangeType) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $argType), new AccessoryArrayListType()); + } + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new IntegerType()), new AccessoryArrayListType()); } - $isFloat = (new FloatType())->isSuperTypeOf($argType)->yes(); - if ($isFloat) { - return new ArrayType(new IntegerType(), new FloatType()); + if ($argType->isFloat()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new FloatType()), new AccessoryArrayListType()); } $numberType = new UnionType([new IntegerType(), new FloatType()]); $isNumber = $numberType->isSuperTypeOf($argType)->yes(); $isNumericString = $argType->isNumericString()->yes(); if ($isNumber || $isNumericString) { - return new ArrayType(new IntegerType(), $numberType); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $numberType), new AccessoryArrayListType()); } - $isString = (new StringType())->isSuperTypeOf($argType)->yes(); - if ($isString) { - return new ArrayType(new IntegerType(), new StringType()); + if ($argType->isString()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } - return new ArrayType(new IntegerType(), new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()])); + return TypeCombinator::intersect(new ArrayType( + new IntegerType(), + new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()]), + ), new AccessoryArrayListType()); } } diff --git a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php index 3a3fbeb13b..94f0d95379 100644 --- a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -4,27 +4,20 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClassStringType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use ReflectionClass; +use function count; -class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) - { - $this->reflectionProvider = $reflectionProvider; - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClass::class; @@ -37,22 +30,15 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::flattenTypes($valueType) as $type) { - if ($type instanceof ClassStringType || $type instanceof ObjectWithoutClassType || $type instanceof ObjectType) { - continue; - } - - if ( - $type instanceof ConstantStringType - && $this->reflectionProvider->hasClass($type->getValue()) - ) { - continue; - } - - return $methodReflection->getThrowType(); + $classOrString = new UnionType([ + new ClassStringType(), + new ObjectWithoutClassType(), + ]); + if ($classOrString->isSuperTypeOf($valueType)->yes()) { + return null; } - return null; + return $methodReflection->getThrowType(); } } diff --git a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php index 601366a9f8..2d830e1501 100644 --- a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php +++ b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php @@ -8,13 +8,16 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MethodTypeSpecifyingExtension; -use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; +use ReflectionClass; -class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -26,32 +29,45 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return \ReflectionClass::class; + return ReflectionClass::class; } public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool { return $methodReflection->getName() === 'isSubclassOf' && isset($node->getArgs()[0]) - && $context->true(); + && !$context->null(); } public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $calledOnType = $scope->getType($node->var); + $reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T'); + if (!(new ObjectWithoutClassType())->isSuperTypeOf($reflectionType)->yes()) { + return new SpecifiedTypes(); + } + $valueType = $scope->getType($node->getArgs()[0]->value); - if (!$valueType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); + $objectType = $valueType->getClassStringObjectType(); + + $intersected = TypeCombinator::intersect($reflectionType, $objectType); + $narrowingType = new GenericObjectType(ReflectionClass::class, [$intersected]); + + if ($reflectionType->isSuperTypeOf($objectType)->no()) { + return $this->typeSpecifier->create( + $node->var, + $narrowingType, + $context, + $scope, + ); } return $this->typeSpecifier->create( $node->var, - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType($valueType->getValue()), - ]), + $narrowingType, $context, - false, - $scope - ); + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index b25efa7a86..01fe08f354 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -5,23 +5,22 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionFunction; +use function count; -class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -36,7 +35,11 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { + if ($constantString->getValue() === '') { + return null; + } + if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php index 7b0bcf7c17..493ec0e9c3 100644 --- a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -5,28 +5,24 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\Type\IntegerType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use ReflectionAttribute; +use function count; -class ReflectionGetAttributesMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class ReflectionGetAttributesMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var class-string */ - private string $className; - /** * @param class-string $className One of reflection classes: https://www.php.net/manual/en/book.reflection.php */ - public function __construct(string $className) + public function __construct(private string $className) { - $this->className = $className; } public function getClass(): string @@ -36,34 +32,19 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'getAttributes'; + return $methodReflection->getDeclaringClass()->getName() === $this->className + && $methodReflection->getName() === 'getAttributes'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { if (count($methodCall->getArgs()) === 0) { - return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + return null; } $argType = $scope->getType($methodCall->getArgs()[0]->value); + $classType = $argType->getClassStringObjectType(); - if ($argType instanceof ConstantStringType) { - $classType = new ObjectType($argType->getValue()); - } elseif ($argType instanceof GenericClassStringType) { - $classType = $argType->getGenericType(); - } else { - return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); - } - - return new ArrayType(new MixedType(), new GenericObjectType(\ReflectionAttribute::class, [$classType])); - } - - private function getDefaultReturnType(Scope $scope, MethodCall $methodCall, MethodReflection $methodReflection): Type - { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new GenericObjectType(ReflectionAttribute::class, [$classType])), new AccessoryArrayListType()); } } diff --git a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php index 4042fbec7a..a2371b6434 100644 --- a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantStringType; @@ -14,15 +15,14 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use ReflectionMethod; +use function count; -class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -40,7 +40,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $propertyType = $scope->getType($methodCall->getArgs()[1]->value); foreach (TypeUtils::flattenTypes($valueType) as $type) { if ($type instanceof GenericClassStringType) { - $classes = $type->getReferencedClasses(); + $classes = $type->getGenericType()->getObjectClassNames(); } elseif ( $type instanceof ConstantStringType && $this->reflectionProvider->hasClass($type->getValue()) @@ -52,7 +52,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($classes as $class) { $classReflection = $this->reflectionProvider->getClass($class); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { if (!$classReflection->hasMethod($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } @@ -67,7 +67,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index 6d6d53c6de..64392e86a1 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -4,23 +4,22 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionProperty; +use function count; -class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -36,14 +35,14 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $valueType = $scope->getType($methodCall->getArgs()[0]->value); $propertyType = $scope->getType($methodCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { if (!$this->reflectionProvider->hasClass($constantString->getValue())) { return $methodReflection->getThrowType(); } $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { - if (!$classReflection->hasProperty($constantPropertyString->getValue())) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { + if (!$classReflection->hasInstanceProperty($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } } @@ -56,7 +55,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php new file mode 100644 index 0000000000..11c3087222 --- /dev/null +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -0,0 +1,500 @@ +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 a9c6acb7d0..7d1242ffea 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -4,45 +4,64 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +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 +#[AutowiredService] +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,35 +71,168 @@ public function getTypeFromFunctionCall( private function getPreliminarilyResolvedTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - $argumentPosition = $this->functions[$functionReflection->getName()]; - if (count($functionCall->getArgs()) <= $argumentPosition) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + if ($subjectArgumentType === null) { + return $defaultReturnType; } - $subjectArgumentType = $scope->getType($functionCall->getArgs()[$argumentPosition]->value); - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $stringType = new StringType(); - $arrayType = new ArrayType(new MixedType(), new MixedType()); - - $isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType); - $isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType); - $compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType); - if ($compareSuperTypes === $isStringSuperType) { - return $stringType; - } elseif ($compareSuperTypes === $isArraySuperType) { - if ($subjectArgumentType instanceof ArrayType) { - return $subjectArgumentType->generalizeValues(); + + $replaceArgumentType = null; + 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(); + } + } + } + + $result = []; + + if ($subjectArgumentType->isString()->yes()) { + $stringArgumentType = $subjectArgumentType; + } else { + $stringArgumentType = TypeCombinator::intersect(new StringType(), $subjectArgumentType); + } + if ($stringArgumentType->isString()->yes()) { + $result[] = $this->getReplaceType($stringArgumentType, $replaceArgumentType); + } + + if ($subjectArgumentType->isArray()->yes()) { + $arrayArgumentType = $subjectArgumentType; + } else { + $arrayArgumentType = TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), $subjectArgumentType); + } + if ($arrayArgumentType->isArray()->yes()) { + $keyShouldBeOptional = in_array( + $functionReflection->getName(), + ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], + true, + ); + + $constantArrays = $arrayArgumentType->getConstantArrays(); + if ($constantArrays !== []) { + foreach ($constantArrays as $constantArray) { + $valueTypes = $constantArray->getValueTypes(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($constantArray->getKeyTypes() as $index => $keyType) { + $builder->setOffsetValueType( + $keyType, + $this->getReplaceType($valueTypes[$index], $replaceArgumentType), + $keyShouldBeOptional || $constantArray->isOptionalKey($index), + ); + } + $result[] = $builder->getArray(); + } + } else { + $newArrayType = new ArrayType( + $arrayArgumentType->getIterableKeyType(), + $this->getReplaceType($arrayArgumentType->getIterableValueType(), $replaceArgumentType), + ); + if ($arrayArgumentType->isList()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType()); + } + if ($arrayArgumentType->isIterableAtLeastOnce()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + $result[] = $newArrayType; + } + } + + return TypeCombinator::union(...$result); + } + + private function getReplaceType( + Type $subjectArgumentType, + ?Type $replaceArgumentType, + ): Type + { + if ($replaceArgumentType === null) { + return new StringType(); + } + + $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); + } + + return new StringType(); + } + + 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; } - return $subjectArgumentType; } - return $defaultReturnType; + $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..88f95bf308 --- /dev/null +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -0,0 +1,101 @@ +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..32373fcf7a --- /dev/null +++ b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php @@ -0,0 +1,92 @@ +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 0cce9f5365..e308b37687 100644 --- a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -12,8 +13,10 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use SimpleXMLElement; +use function count; -class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +#[AutowiredService] +final class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php index 9646898c00..1bf6d9854b 100644 --- a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php +++ b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php @@ -2,24 +2,27 @@ namespace PHPStan\Type\Php; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; 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 +#[AutowiredService] +final class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension { public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return $classReflection->getName() === 'SimpleXMLElement' || $classReflection->isSubclassOf('SimpleXMLElement'); + return $classReflection->is('SimpleXMLElement'); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { - return new SimpleXMLElementProperty($classReflection, new ObjectType($classReflection->getName())); + return new SimpleXMLElementProperty($propertyName, $classReflection, new BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); } } diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index 9b02e64f3c..78faf2d1d3 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -4,20 +4,26 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use SimpleXMLElement; +use function count; +use function extension_loaded; +use function libxml_use_internal_errors; -class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; + return extension_loaded('simplexml') + && $methodReflection->getName() === '__construct' + && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; } public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type @@ -27,16 +33,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); - foreach ($constantStrings as $constantString) { - try { - new SimpleXMLElement($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); - } + $internalErrorsOld = libxml_use_internal_errors(true); + + try { + foreach ($constantStrings as $constantString) { + try { + new SimpleXMLElement($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $methodReflection->getThrowType(); + } - $valueType = TypeCombinator::remove($valueType, $constantString); + $valueType = TypeCombinator::remove($valueType, $constantString); + } + } finally { + libxml_use_internal_errors($internalErrorsOld); } if (!$valueType instanceof NeverType) { diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index 404514b184..45715f21be 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -4,53 +4,59 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\MixedType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use SimpleXMLElement; +use function extension_loaded; -class SimpleXMLElementXpathMethodReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +#[AutowiredService] +final class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string { - return \SimpleXMLElement::class; + return SimpleXMLElement::class; } public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'xpath'; + return extension_loaded('simplexml') && $methodReflection->getName() === 'xpath'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $args = $methodCall->getArgs(); + if (!isset($args[0])) { + return null; } - $argType = $scope->getType($methodCall->getArgs()[0]->value); + $argType = $scope->getType($args[0]->value); - $xmlElement = new \SimpleXMLElement(''); + $xmlElement = new SimpleXMLElement(''); - foreach (TypeUtils::getConstantStrings($argType) as $constantString) { + foreach ($argType->getConstantStrings() as $constantString) { $result = @$xmlElement->xpath($constantString->getValue()); if ($result === false) { // We can't be sure since it's maybe a namespaced xpath - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $argType = TypeCombinator::remove($argType, $constantString); } if (!$argType instanceof NeverType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } - return new ArrayType(new MixedType(), $scope->getType($methodCall->var)); + $variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants()); + + return TypeCombinator::remove($variant->getReturnType(), new ConstantBooleanType(false)); } } diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 2ca19905c8..53a93c1dc2 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -2,68 +2,374 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use Throwable; +use function array_fill; +use function array_key_exists; +use function array_shift; +use function array_values; +use function count; +use function in_array; +use function intval; +use function is_array; +use function is_string; +use function preg_match; +use function sprintf; +use function substr; +use function vsprintf; -class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +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 ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; + } + + $constantType = $this->getConstantType($args, $functionReflection, $scope); + if ($constantType !== null) { + return $constantType; } $formatType = $scope->getType($args[0]->value); - if ($formatType->isNonEmptyString()->yes()) { - $returnType = new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + $formatStrings = $formatType->getConstantStrings(); + + $isLowercase = $formatType->isLowercaseString()->yes() && $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes(), + ); + + $singlePlaceholderEarlyReturn = []; + $allPatternsNonEmpty = count($formatStrings) !== 0; + $allPatternsNonFalsy = count($formatStrings) !== 0; + foreach ($formatStrings as $constantString) { + $constantParts = $this->getFormatConstantParts( + $constantString->getValue(), + $functionReflection, + $functionCall, + $scope, + ); + if ($constantParts !== null) { + if ($constantParts->isNonFalsyString()->yes()) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf + // keep all bool flags as is + } elseif ($constantParts->isNonEmptyString()->yes()) { + $allPatternsNonFalsy = false; + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + + if ( + is_array($singlePlaceholderEarlyReturn) + // The printf format is %[argnum$][flags][width][.precision]specifier. + && preg_match('/^%(?P[0-9]*\$)?(?P[0-9]*)\.?[0-9]*(?P[sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1 + ) { + if ($matches['argnum'] !== '') { + // invalid positional argument + if ($matches['argnum'] === '0$') { + return null; + } + $checkArg = intval(substr($matches['argnum'], 0, -1)); + } else { + $checkArg = 1; + } + + $checkArgType = $this->getValueType($functionReflection, $scope, $args, $checkArg); + if ($checkArgType === null) { + return null; + } + + // if the format string is just a placeholder and specified an argument + // of stringy type, then the return value will be of the same type + if ( + $matches['specifier'] === 's' + && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) + ) { + if ($checkArgType instanceof IntegerRangeType) { + $constArgTypes = $checkArgType->getFiniteTypes(); + } else { + $constArgTypes = $checkArgType->getConstantScalarTypes(); + } + if ($constArgTypes !== []) { + $printfArgs = array_fill(0, count($args) - 1, ''); + foreach ($constArgTypes as $constArgType) { + $printfArgs[$checkArg - 1] = $constArgType->getValue(); + try { + $singlePlaceholderEarlyReturn[] = new ConstantStringType(@sprintf($constantString->getValue(), ...$printfArgs)); + } catch (Throwable) { + continue 2; + } + } + + continue; + } + + $singlePlaceholderEarlyReturn[] = $checkArgType->toString(); + } elseif ($matches['specifier'] !== 's') { + $singlePlaceholderEarlyReturn[] = $this->getStringReturnType( + new AccessoryNumericStringType(), + $isLowercase, + ); + } + + continue; + } + + $singlePlaceholderEarlyReturn = null; + } + + if (is_array($singlePlaceholderEarlyReturn) && count($singlePlaceholderEarlyReturn) > 0) { + return TypeCombinator::union(...$singlePlaceholderEarlyReturn); + } + + if ($allPatternsNonFalsy) { + return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase); + } + + $isNonEmpty = $allPatternsNonEmpty; + if (!$isNonEmpty && $formatType->isNonEmptyString()->yes()) { + $isNonEmpty = $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isNonEmptyString()->yes(), + ); + } + + if ($isNonEmpty) { + return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase); + } + + return $this->getStringReturnType(null, $isLowercase); + } + + /** + * @param array $args + * @param callable(Type): bool $cb + */ + private function allValuesSatisfies(FunctionReflection $functionReflection, Scope $scope, array $args, callable $cb): bool + { + if ($functionReflection->getName() === 'sprintf' && count($args) >= 2) { + foreach ($args as $key => $arg) { + if ($key === 0) { + continue; + } + + if (!$cb($scope->getType($arg->value))) { + return false; + } + } + + return true; + } + + if ($functionReflection->getName() === 'vsprintf' && count($args) >= 2) { + return $cb($scope->getType($args[1]->value)->getIterableValueType()); + } + + return false; + } + + /** + * @param Arg[] $args + */ + private function getValueType(FunctionReflection $functionReflection, Scope $scope, array $args, int $argNumber): ?Type + { + if ($functionReflection->getName() === 'sprintf') { + // constant string specifies a numbered argument that does not exist + if (!array_key_exists($argNumber, $args)) { + return null; + } + + return $scope->getType($args[$argNumber]->value); + } + + if ($functionReflection->getName() === 'vsprintf') { + if (!array_key_exists(1, $args)) { + return null; + } + + $valuesType = $scope->getType($args[1]->value); + $resultTypes = []; + + $valuesConstantArrays = $valuesType->getConstantArrays(); + foreach ($valuesConstantArrays as $valuesConstantArray) { + // vsprintf does not care about the keys of the array, only the order + $types = array_values($valuesConstantArray->getValueTypes()); + if (!array_key_exists($argNumber - 1, $types)) { + return null; + } + + $resultTypes[] = $types[$argNumber - 1]; + } + if (count($resultTypes) === 0) { + return $valuesType->getIterableValueType(); + } + + return TypeCombinator::union(...$resultTypes); + } + + return null; + } + + /** + * Detect constant strings in the format which neither depend on placeholders nor on given value arguments. + */ + private function getFormatConstantParts( + string $format, + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?ConstantStringType + { + $args = $functionCall->getArgs(); + if ($functionReflection->getName() === 'sprintf') { + $valuesCount = count($args) - 1; + } elseif ( + $functionReflection->getName() === 'vsprintf' + && count($args) >= 2 + ) { + $arraySize = $scope->getType($args[1]->value)->getArraySize(); + if (!($arraySize instanceof ConstantIntegerType)) { + return null; + } + + $valuesCount = $arraySize->getValue(); } else { - $returnType = new StringType(); + return null; + } + + if ($valuesCount <= 0) { + return null; } + $dummyValues = array_fill(0, $valuesCount, ''); + + try { + $formatted = @vsprintf($format, $dummyValues); + if ($formatted === false) { // @phpstan-ignore identical.alwaysFalse (PHP7.2 compat) + return null; + } + return new ConstantStringType($formatted); + } catch (Throwable) { + return null; + } + } + /** + * @param Arg[] $args + */ + private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type + { $values = []; + $combinationsCount = 1; foreach ($args as $arg) { + if ($arg->unpack) { + return null; + } + $argType = $scope->getType($arg->value); - if (!$argType instanceof ConstantScalarType) { - return $returnType; + $constantScalarValues = $argType->getConstantScalarValues(); + + if (count($constantScalarValues) === 0) { + if ($argType instanceof IntegerRangeType) { + foreach ($argType->getFiniteTypes() as $finiteType) { + $constantScalarValues[] = $finiteType->getValue(); + } + } + } + + if (count($constantScalarValues) === 0) { + return null; } - $values[] = $argType->getValue(); + $values[] = $constantScalarValues; + $combinationsCount *= count($constantScalarValues); } - $format = array_shift($values); - if (!is_string($format)) { - return $returnType; + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } - try { - $value = @sprintf($format, ...$values); - } catch (\Throwable $e) { - return $returnType; + $combinations = CombinationsHelper::combinations($values); + $returnTypes = []; + foreach ($combinations as $combination) { + $format = array_shift($combination); + if (!is_string($format)) { + return null; + } + + try { + if ($functionReflection->getName() === 'sprintf') { + $returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination)); + } else { + $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); + } + } catch (Throwable) { + return null; + } } - return $scope->getTypeFromValue($value); + if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; + } + + return TypeCombinator::union(...$returnTypes); + } + + private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type + { + $accessoryTypes = []; + if ($accessoryType !== null) { + $accessoryTypes[] = $accessoryType; + } + if ($isLowercase) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if (count($accessoryTypes) === 0) { + return new StringType(); + } + + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); } } diff --git a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..de22ba0a46 --- /dev/null +++ b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,91 @@ +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..b4dcd8bb7e 100644 --- a/src/Type/Php/StatDynamicReturnTypeExtension.php +++ b/src/Type/Php/StatDynamicReturnTypeExtension.php @@ -5,16 +5,22 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; 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 +#[AutowiredService] +final class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,12 +30,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 +76,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..c965f8cd12 --- /dev/null +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -0,0 +1,172 @@ + 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..2e24c9dcbd --- /dev/null +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -0,0 +1,111 @@ + [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..f399327f46 --- /dev/null +++ b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php @@ -0,0 +1,102 @@ +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; + + $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 NeverType() + : TypeCombinator::union(...$types); + } + + private function increment(string $s): ?string + { + if ($s === '') { + return null; + } + + try { + return str_increment($s); + } catch (ValueError) { + return null; + } + } + + private function decrement(string $s): ?string + { + if ($s === '') { + return null; + } + + try { + return str_decrement($s); + } catch (ValueError) { + return null; + } + } + +} diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php index cd239cc4a6..a6e9f927c9 100644 --- a/src/Type/Php/StrPadFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -4,15 +4,22 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use function count; -class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,8 +30,8 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): Type { $args = $functionCall->getArgs(); if (count($args) < 2) { @@ -35,19 +42,26 @@ public function getTypeFromFunctionCall( $lengthType = $scope->getType($args[1]->value); $accessoryTypes = []; - if ($inputType->isNonEmptyString()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) { + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($inputType->isNonEmptyString()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } - if ($inputType->isLiteralString()->yes()) { - if (count($args) < 3) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } else { - $padStringType = $scope->getType($args[2]->value); - if ($padStringType->isLiteralString()->yes()) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } - } + if (count($args) < 3) { + $padStringType = null; + } else { + $padStringType = $scope->getType($args[2]->value); + } + + if ($inputType->isLiteralString()->yes() && ($padStringType === null || $padStringType->isLiteralString()->yes())) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + if ($inputType->isLowercaseString()->yes() && ($padStringType === null || $padStringType->isLowercaseString()->yes())) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($inputType->isUppercaseString()->yes() && ($padStringType === null || $padStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); } if (count($accessoryTypes) > 0) { diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index a66753a1a4..585c02bf9b 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -2,19 +2,31 @@ namespace PHPStan\Type\Php; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use function count; +use function str_repeat; +use function strlen; -class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -25,37 +37,78 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): Type { $args = $functionCall->getArgs(); if (count($args) < 2) { return new StringType(); } - $inputType = $scope->getType($args[0]->value); $multiplierType = $scope->getType($args[1]->value); if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) { return new ConstantStringType(''); } + if (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()) { - $accessoryTypes[] = new AccessoryNonEmptyStringType(); + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } else { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } } } if ($inputType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); + + if ( + $inputType->isNumericString()->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes() + ) { + $onlyNumbers = true; + foreach ($inputType->getConstantStrings() as $constantString) { + if (Strings::match($constantString->getValue(), '#^[0-9]+$#') === null) { + $onlyNumbers = false; + break; + } + } + + if ($onlyNumbers) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } + } + + if ($inputType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if ($inputType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); } - return new StringType(); } diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 78292f54f5..0c4a31496c 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -4,8 +4,13 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +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\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -13,31 +18,29 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; - +use function array_is_list; +use function array_map; +use function array_unique; +use function count; +use function in_array; +use function mb_internal_encoding; +use function mb_str_split; +use function str_split; + +#[AutowiredService] final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $supportedEncodings; + use MbFunctionsReturnTypeExtensionTrait; - public function __construct() + public function __construct(private PhpVersion $phpVersion) { - $supportedEncodings = []; - if (function_exists('mb_list_encodings')) { - foreach (mb_list_encodings() as $encoding) { - $aliases = mb_encoding_aliases($encoding); - if ($aliases === false) { - throw new \PHPStan\ShouldNotHappenException(); - } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); - } - } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -45,78 +48,99 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 1) { - return $defaultReturnType; + return null; } if (count($functionCall->getArgs()) >= 2) { $splitLengthType = $scope->getType($functionCall->getArgs()[1]->value); - if ($splitLengthType instanceof ConstantIntegerType) { - $splitLength = $splitLengthType->getValue(); - if ($splitLength < 1) { - return new ConstantBooleanType(false); - } - } } else { - $splitLength = 1; + $splitLengthType = new ConstantIntegerType(1); + } + + if ($splitLengthType instanceof ConstantIntegerType) { + $splitLength = $splitLengthType->getValue(); + if ($splitLength < 1) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + } } + $encoding = null; if ($functionReflection->getName() === 'mb_str_split') { if (count($functionCall->getArgs()) >= 3) { - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); - $values = array_unique(array_map(static function (ConstantStringType $encoding): string { - return $encoding->getValue(); - }, $strings)); - - if (count($values) !== 1) { - return $defaultReturnType; - } - - $encoding = $values[0]; - if (!$this->isSupportedEncoding($encoding)) { - return new ConstantBooleanType(false); + $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) { + $encoding = $values[0]; + if (!$this->isSupportedEncoding($encoding)) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + } } } else { $encoding = mb_internal_encoding(); } } - if (!isset($splitLength)) { - return $defaultReturnType; - } - $stringType = $scope->getType($functionCall->getArgs()[0]->value); - if (!$stringType instanceof ConstantStringType) { - return TypeCombinator::intersect( - new ArrayType(new IntegerType(), new StringType()), - new NonEmptyArrayType() - ); + if ( + isset($splitLength) + && ($functionReflection->getName() === 'str_split' || $encoding !== null) + ) { + $constantStrings = $stringType->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + foreach ($constantStrings as $constantString) { + $value = $constantString->getValue(); + + if ($encoding === null && $value === '') { + // Simulate the str_split call with the analysed PHP Version instead of the runtime one. + $items = $this->phpVersion->strSplitReturnsEmptyArray() ? [] : ['']; + } else { + $items = $encoding === null + ? str_split($value, $splitLength) + : @mb_str_split($value, $splitLength, $encoding); + } + + $results[] = self::createConstantArrayFrom($items, $scope); + } + + return TypeCombinator::union(...$results); + } } - $stringValue = $stringType->getValue(); - $items = isset($encoding) - ? mb_str_split($stringValue, $splitLength, $encoding) - : str_split($stringValue, $splitLength); - if ($items === false) { - throw new \PHPStan\ShouldNotHappenException(); + $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); + + if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { + $returnValueType = TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()); + } else { + $returnValueType = new StringType(); } - return self::createConstantArrayFrom($items, $scope); - } + $returnType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType), new AccessoryArrayListType()); + if ( + // Non-empty-string will return an array with at least an element + $isInputNonEmptyString + // str_split('', 1) returns [''] on old PHP version and [] on new ones + || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray()) + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + if ( + // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version. + !$this->phpVersion->throwsValueErrorForInternalFunctions() + && !IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($splitLengthType)->yes() + ) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); + } - private function isSupportedEncoding(string $encoding): bool - { - return in_array(strtoupper($encoding), $this->supportedEncodings, true); + return $returnType; } /** * @param string[] $constantArray - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Type\Constant\ConstantArrayType */ private static function createConstantArrayFrom(array $constantArray, Scope $scope): ConstantArrayType { @@ -128,7 +152,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; @@ -138,7 +162,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], isList: TrinaryLogic::createFromBoolean(array_is_list($constantArray))); } } diff --git a/src/Type/Php/StrTokFunctionReturnTypeExtension.php b/src/Type/Php/StrTokFunctionReturnTypeExtension.php index 5a3bee5146..01af622932 100644 --- a/src/Type/Php/StrTokFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -4,14 +4,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use function count; -class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,11 +24,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'strtok'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): \PHPStan\Type\Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); if (count($args) !== 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,10 +38,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($isEmptyString->no()) { - return new StringType(); + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index c35dac7758..a53b2c5c7d 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,24 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; 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; +use function in_array; -class StrWordCountFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,21 +29,21 @@ 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->getArgs()); if ($argsCount === 1) { return new IntegerType(); - } elseif ($argsCount === 2 || $argsCount === 3) { + } elseif (in_array($argsCount, [2, 3], true)) { $formatType = $scope->getType($functionCall->getArgs()[1]->value); if ($formatType instanceof ConstantIntegerType) { $val = $formatType->getValue(); if ($val === 0) { // return word count return new IntegerType(); - } elseif ($val === 1 || $val === 2) { + } elseif (in_array($val, [1, 2], true)) { // return [word] or [offset => word] return new ArrayType(new IntegerType(), new StringType()); } diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php index f92aa6abe6..c758d49095 100644 --- a/src/Type/Php/StrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -4,16 +4,26 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\FloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\TypeUtils; - -class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_map; +use function array_unique; +use function count; +use function max; +use function min; +use function range; +use function sort; +use function strlen; + +#[AutowiredService] +final class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,58 +34,48 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($args[0]->value); + $constantScalars = $argType->getConstantScalarValues(); - $constantStrings = TypeUtils::getConstantStrings($argType); - $min = null; - $max = null; - foreach ($constantStrings as $constantString) { - $len = strlen($constantString->getValue()); - - if ($min === null) { - $min = $len; - $max = $len; - } - - if ($len < $min) { - $min = $len; - } - if ($len <= $max) { - continue; - } - - $max = $len; - } - - // $max is always != null, when $min is != null - if ($min !== null) { - return IntegerRangeType::fromInterval($min, $max); - } - - $bool = new BooleanType(); - if ($bool->isSuperTypeOf($argType)->yes()) { - return IntegerRangeType::fromInterval(0, 1); + $lengths = []; + foreach ($constantScalars as $constantScalar) { + $stringScalar = (string) $constantScalar; + $length = strlen($stringScalar); + $lengths[] = $length; } $isNonEmpty = $argType->isNonEmptyString(); - $integer = new IntegerType(); - if ($isNonEmpty->yes() || $integer->isSuperTypeOf($argType)->yes()) { - return IntegerRangeType::fromInterval(1, null); - } - - if ($isNonEmpty->no()) { - return new ConstantIntegerType(0); + $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); + $range = null; + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( + $isNonEmpty->yes() + || $numeric->isSuperTypeOf($argType)->yes() + || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() + ) { + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $range; } } diff --git a/src/Type/Php/StrrevFunctionReturnTypeExtension.php b/src/Type/Php/StrrevFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..4bbc2c8fb8 --- /dev/null +++ b/src/Type/Php/StrrevFunctionReturnTypeExtension.php @@ -0,0 +1,75 @@ +getName() === 'strrev'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $inputType = $scope->getType($args[0]->value); + $constantStrings = $inputType->getConstantStrings(); + if (count($constantStrings) > 0) { + $resultTypes = []; + foreach ($constantStrings as $constantString) { + $resultTypes[] = new ConstantStringType(strrev($constantString->getValue())); + } + + return TypeCombinator::union(...$resultTypes); + } + + $accessoryTypes = []; + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($inputType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + 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 null; + } + +} diff --git a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php index 791392e78c..284759e59e 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -4,16 +4,26 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\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 +#[AutowiredService] +final class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,7 +33,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); if (count($functionCall->getArgs()) === 0) { return $defaultReturnType; } @@ -31,15 +45,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, 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 index 7f2b431a94..3fa0403bab 100644 --- a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -4,12 +4,20 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function count; +use function in_array; -class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const FUNCTIONS = [ @@ -28,7 +36,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { if (count($functionCall->getArgs()) === 0) { @@ -41,14 +49,16 @@ public function getTypeFromFunctionCall( case 'strval': return $argType->toString(); case 'intval': - return $argType->toInteger(); + $type = $argType->toInteger(); + return $type instanceof ErrorType ? new IntegerType() : $type; case 'boolval': return $argType->toBoolean(); case 'floatval': case 'doubleval': - return $argType->toFloat(); + $type = $argType->toFloat(); + return $type instanceof ErrorType ? new FloatType() : $type; default: - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 20c95ec2b1..002340465b 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -4,56 +4,164 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; +use function in_array; +use function is_bool; +use function mb_substr; +use function strlen; +use function substr; -class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'substr'; + return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): \PHPStan\Type\Type + Scope $scope, + ): ?Type { $args = $functionCall->getArgs(); - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($args) < 2) { + return null; + } + + $string = $scope->getType($args[0]->value); + $offset = $scope->getType($args[1]->value); + + $negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes(); + $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); + $length = null; + $positiveLength = false; + $maybeOneLength = false; + + if (count($args) === 3) { + $length = $scope->getType($args[2]->value); + $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + $maybeOneLength = !(new ConstantIntegerType(1))->isSuperTypeOf($length)->no(); } - if (count($args) >= 2) { - $string = $scope->getType($args[0]->value); - $offset = $scope->getType($args[1]->value); + $constantStrings = $string->getConstantStrings(); + if ( + count($constantStrings) > 0 + && $offset instanceof ConstantIntegerType + && ($length === null || $length instanceof ConstantIntegerType) + ) { + $results = []; + foreach ($constantStrings as $constantString) { + if ($length !== null) { + if ($functionReflection->getName() === 'mb_substr') { + $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + $substr = $this->substrOrFalse($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()); + } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + // Simulate substr call on an older PHP version if the runtime one is too new. + $substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue()); + } else { + $substr = substr($constantString->getValue(), $offset->getValue()); + } + } - $negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes(); - $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); - $positiveLength = false; + if (is_bool($substr)) { + if ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + $results[] = new ConstantBooleanType($substr); + } else { + // Simulate substr call on a recent PHP version if the runtime one is too old. + $results[] = new ConstantStringType(''); + } + } 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 (count($args) === 3) { - $length = $scope->getType($args[2]->value); - $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + return TypeCombinator::union( + new ConstantBooleanType(false), + new IntersectionType($accessoryTypes), + ); } - if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + return new IntersectionType($accessoryTypes); + } + + return null; + } + + private function substrOrFalse(string $string, int $offset, ?int $length = null): false|string + { + $strlen = strlen($string); + + if ($offset > $strlen) { + return false; + } + + if ($length !== null && $length < 0) { + if ($offset < 0 && -$length > $strlen) { + return false; } + if ($offset >= 0 && -$length > $strlen - $offset) { + return false; + } + } + + if ($length === null) { + return substr($string, $offset); } - return new StringType(); + return substr($string, $offset, $length); } } diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php new file mode 100644 index 0000000000..af236b4f2d --- /dev/null +++ b/src/Type/Php/ThrowableReturnTypeExtension.php @@ -0,0 +1,79 @@ +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..3f3e09242d --- /dev/null +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -0,0 +1,70 @@ +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..fd016b7650 --- /dev/null +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,93 @@ +getName(), ['trim', 'rtrim'], 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 = []; + $defaultType = new StringType(); + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $defaultType = new IntersectionType($accessory); + } + + if (count($functionCall->getArgs()) !== 2) { + return $defaultType; + } + + $trimChars = $scope->getType($functionCall->getArgs()[1]->value); + + $trimConstantStrings = $trimChars->getConstantStrings(); + if (count($trimConstantStrings) > 0) { + $result = []; + $stringConstantStrings = $stringType->getConstantStrings(); + $functionName = $functionReflection->getName(); + + foreach ($trimConstantStrings as $trimConstantString) { + if (count($stringConstantStrings) > 0) { + foreach ($stringConstantStrings as $stringConstantString) { + $result[] = new ConstantStringType( + $functionName === 'rtrim' + ? rtrim($stringConstantString->getValue(), $trimConstantString->getValue()) + : trim($stringConstantString->getValue(), $trimConstantString->getValue()), + true, + ); + } + } elseif (preg_match('/\d/', $trimConstantString->getValue()) === 0 && $stringType->isNumericString()->yes()) { + $result[] = new AccessoryNumericStringType(); + } else { + return $defaultType; + } + } + + return TypeCombinator::union(...$result); + } + + return $defaultType; + } + +} diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index 08fe95d210..8ed3c6b605 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -6,38 +6,36 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; 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 +#[AutowiredService] +final class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension { - private bool $treatPhpDocTypesAsCertain; + private TypeSpecifier $typeSpecifier; - private ReflectionProvider $reflectionProvider; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private ?\PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $helper = null; - - /** @var string[] */ - private array $universalObjectCratesClasses; + private ?ImpossibleCheckTypeHelper $helper = null; /** - * @param ReflectionProvider $reflectionProvider - * @param bool $treatPhpDocTypesAsCertain * @param string[] $universalObjectCratesClasses */ - public function __construct(ReflectionProvider $reflectionProvider, bool $treatPhpDocTypesAsCertain, array $universalObjectCratesClasses) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private array $universalObjectCratesClasses, + ) { - $this->reflectionProvider = $reflectionProvider; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -49,42 +47,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->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $isAlways = $this->getHelper()->findSpecifiedType( $scope, - $functionCall + $functionCall, ); if ($isAlways === null) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } return new ConstantBooleanType($isAlways); @@ -92,11 +76,7 @@ public function getTypeFromFunctionCall( private function getHelper(): ImpossibleCheckTypeHelper { - if ($this->helper === null) { - $this->helper = new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); - } - - return $this->helper; + return $this->helper ??= new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); } } diff --git a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php deleted file mode 100644 index de6b3cbc12..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->getArgs()) < 1) { - return TypeCombinator::union( - new StringType(), - $fallbackReturnType - ); - } - - if (count($functionCall->getArgs()) < 2) { - return $fallbackReturnType; - } - - $returnArgumentType = $scope->getType($functionCall->getArgs()[1]->value); - if ((new ConstantBooleanType(true))->isSuperTypeOf($returnArgumentType)->yes()) { - return new StringType(); - } - - return $fallbackReturnType; - } - -} diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index 62347e0f75..d177092349 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -2,20 +2,62 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\BenevolentUnionType; 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\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function array_filter; +use function count; +use function in_array; +use function is_array; +use function version_compare; -class VersionCompareFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const VALID_OPERATORS = [ + '<', + 'lt', + '<=', + 'le', + '>', + 'gt', + '>=', + 'ge', + '==', + '=', + 'eq', + '!=', + '<>', + 'ne', + ]; + + /** + * @param int|array{min: int, max: int}|null $configPhpVersion + */ + public function __construct( + #[AutowiredParameter(ref: '%phpVersion%')] + private int|array|null $configPhpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, + private PhpVersion $phpVersion, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'version_compare'; @@ -24,50 +66,59 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[0]->value)); - $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)); + $version1Strings = $this->getVersionStrings($args[0]->value, $scope); + $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ count($version1Strings), count($version2Strings), ]; - if (isset($functionCall->getArgs()[2])) { - $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + if (isset($args[2])) { + $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); - $returnType = new BooleanType(); + $returnType = $this->phpVersion->throwsValueErrorForInternalFunctions() + ? new BooleanType() + : new BenevolentUnionType([new BooleanType(), new NullType()]); } 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 } $types = []; + $canBeNull = false; foreach ($version1Strings as $version1String) { foreach ($version2Strings as $version2String) { if (isset($operatorStrings)) { foreach ($operatorStrings as $operatorString) { - $value = version_compare($version1String->getValue(), $version2String->getValue(), $operatorString->getValue()); + $operatorValue = $operatorString->getValue(); + if (!in_array($operatorValue, self::VALID_OPERATORS, true)) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $canBeNull = true; + } + + continue; + } + + $value = version_compare($version1String->getValue(), $version2String->getValue(), $operatorValue); $types[$value] = new ConstantBooleanType($value); } } else { @@ -76,7 +127,40 @@ public function getTypeFromFunctionCall( } } } + + if ($canBeNull) { + $types[] = new NullType(); + } + return TypeCombinator::union(...$types); } + /** + * @return ConstantStringType[] + */ + private function getVersionStrings(Expr $expr, Scope $scope): array + { + if ( + $expr instanceof Expr\ConstFetch + && $expr->name->toString() === 'PHP_VERSION' + ) { + if (is_array($this->configPhpVersion)) { + $minVersion = new PhpVersion($this->configPhpVersion['min']); + $maxVersion = new PhpVersion($this->configPhpVersion['max']); + } else { + $minVersion = $this->composerPhpVersionFactory->getMinVersion(); + $maxVersion = $this->composerPhpVersionFactory->getMaxVersion(); + } + + if ($minVersion !== null && $maxVersion !== null) { + return [ + new ConstantStringType($minVersion->getVersionString()), + new ConstantStringType($maxVersion->getVersionString()), + ]; + } + } + + return $scope->getType($expr)->getConstantStrings(); + } + } diff --git a/src/Type/Php/XMLReaderOpenReturnTypeExtension.php b/src/Type/Php/XMLReaderOpenReturnTypeExtension.php index 62e820b703..e5283ba68e 100644 --- a/src/Type/Php/XMLReaderOpenReturnTypeExtension.php +++ b/src/Type/Php/XMLReaderOpenReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,7 +15,8 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class XMLReaderOpenReturnTypeExtension implements DynamicMethodReturnTypeExtension, DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class XMLReaderOpenReturnTypeExtension implements DynamicMethodReturnTypeExtension, DynamicStaticMethodReturnTypeExtension { private const XML_READER_CLASS = 'XMLReader'; diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index 6e1cf7191a..23682660a9 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -2,19 +2,20 @@ namespace PHPStan\Type; -class RecursionGuard +use function spl_object_hash; + +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])) { @@ -29,4 +30,24 @@ public static function run(Type $type, callable $callback): Type } } + /** + * @template T + * @param callable(): T $callback + * @return T|ErrorType + */ + public static function runOnObjectIdentity(Type $type, callable $callback) + { + $key = spl_object_hash($type); + if (isset(self::$context[$key])) { + return new ErrorType(); + } + + try { + self::$context[$key] = true; + return $callback(); + } finally { + unset(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..0dd32830cd --- /dev/null +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -0,0 +1,164 @@ +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..a273753be8 --- /dev/null +++ b/src/Type/Regex/RegexGroupList.php @@ -0,0 +1,169 @@ + + */ +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; + } + + #[Override] + public function count(): int + { + return count($this->groups); + } + + /** + * @return ArrayIterator + */ + #[Override] + 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..3162a1c783 --- /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'], parent: $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', parent: $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 (in_array($token, ['anchor', 'match_point_reset'], true)) { + 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 7cb78a65ad..01e7fe86e0 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,13 +2,20 @@ 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; @@ -17,12 +24,16 @@ 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() @@ -34,11 +45,21 @@ public function describe(VerbosityLevel $level): string return 'resource'; } + public function getConstantStrings(): array + { + return []; + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new StringType(); @@ -51,7 +72,7 @@ public function toInteger(): Type public function toFloat(): Type { - return new ErrorType(); + return new FloatType(); } public function toArray(): Type @@ -59,37 +80,49 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: TrinaryLogic::createYes(), ); } - public function isOffsetAccessible(): TrinaryLogic + public function toArrayKey(): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + 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, bool $unionValues = true): 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..d9a3d69783 --- /dev/null +++ b/src/Type/SimultaneousTypeTraverser.php @@ -0,0 +1,42 @@ +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/StaticMethodParameterClosureThisExtension.php b/src/Type/StaticMethodParameterClosureThisExtension.php new file mode 100644 index 0000000000..7abe7bc134 --- /dev/null +++ b/src/Type/StaticMethodParameterClosureThisExtension.php @@ -0,0 +1,33 @@ +classReflection = $classReflection; + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + + $this->subtractedType = $subtractedType; $this->baseClass = $classReflection->getName(); } @@ -45,7 +58,7 @@ public function getClassName(): string return $this->baseClass; } - public function getClassReflection(): ?ClassReflection + public function getClassReflection(): ClassReflection { return $this->classReflection; } @@ -69,67 +82,92 @@ public function getStaticObjectType(): ObjectType { if ($this->staticObjectType === null) { if ($this->classReflection->isGeneric()) { - $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static function (string $name, Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }); + $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->classReflection->typeMapToList($typeMap), + $this->subtractedType, + variances: $this->classReflection->varianceMapToList($varianceMap), ); } - return $this->staticObjectType = new ObjectType($this->classReflection->getName(), null, $this->classReflection); + 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 accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return $this->getStaticObjectType()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->getStaticObjectType()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->getStaticObjectType()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->getStaticObjectType()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->getStaticObjectType()->getConstantStrings(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof static) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } return $this->getStaticObjectType()->accepts($type->getStaticObjectType(), $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { return $this->getStaticObjectType()->isSuperTypeOf($type); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof ObjectType) { $result = $this->getStaticObjectType()->isSuperTypeOf($type); - $classReflection = $type->getClassReflection(); - if ($result->yes() && $classReflection !== null && $classReflection->isFinal()) { - return $result; + if ($result->yes()) { + $classReflection = $type->getClassReflection(); + if ($classReflection !== null && $classReflection->isFinal()) { + return $result; + } } - return TrinaryLogic::createMaybe()->and($result); + return $result->and(IsSuperTypeOfResult::createMaybe()); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -138,14 +176,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 @@ -158,7 +209,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->getStaticObjectType()->hasProperty($propertyName); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -181,9 +232,71 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $nakedProperty, $classReflection, false, - function (Type $type) use ($scope): Type { - return $this->transformStaticType($type, $scope); - } + fn (Type $type): Type => $this->transformStaticType($type, $scope), + ); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->getStaticObjectType()->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $staticObject = $this->getStaticObjectType(); + $nakedProperty = $staticObject->getUnresolvedInstancePropertyPrototype($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 hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->getStaticObjectType()->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $staticObject = $this->getStaticObjectType(); + $nakedProperty = $staticObject->getUnresolvedStaticPropertyPrototype($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), ); } @@ -197,7 +310,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->getStaticObjectType()->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -220,9 +333,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $nakedMethod, $classReflection, false, - function (Type $type) use ($scope): Type { - return $this->transformStaticType($type, $scope); - } + fn (Type $type): Type => $this->transformStaticType($type, $scope), ); } @@ -237,11 +348,11 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop $isFinal = $classReflection->isFinal(); } $type = $type->changeBaseClass($classReflection); - if (!$isFinal) { - return $type; + if (!$isFinal || $type instanceof ThisType) { + return $traverse($type); } - return $type->getStaticObjectType(); + return $traverse($type->getStaticObjectType()); } return $traverse($type); @@ -258,14 +369,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); + return new self($classReflection, $this->subtractedType); } public function isIterable(): TrinaryLogic @@ -278,21 +389,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); @@ -308,16 +449,171 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->getStaticObjectType()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->getStaticObjectType()->setExistingOffsetValueType($offsetType, $valueType); + } + + public function unsetOffset(Type $offsetType): Type + { + return $this->getStaticObjectType()->unsetOffset($offsetType); + } + + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getStaticObjectType()->getKeysArrayFiltered($filterValueType, $strict); + } + + public function getKeysArray(): Type + { + return $this->getStaticObjectType()->getKeysArray(); + } + + public function getValuesArray(): Type + { + return $this->getStaticObjectType()->getValuesArray(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->chunkArray($lengthType, $preserveKeys); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->getStaticObjectType()->fillKeysArray($valueType); + } + + public function flipArray(): Type + { + return $this->getStaticObjectType()->flipArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->getStaticObjectType()->intersectKeyArray($otherArraysType); + } + + public function popArray(): Type + { + return $this->getStaticObjectType()->popArray(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->reverseArray($preserveKeys); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + return $this->getStaticObjectType()->searchArray($needleType, $strict); + } + + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->getStaticObjectType()->spliceArray($offsetType, $lengthType, $replacementType); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); } + public function getEnumCases(): array + { + return $this->getStaticObjectType()->getEnumCases(); + } + public function isArray(): TrinaryLogic { return $this->getStaticObjectType()->isArray(); } + public function isConstantArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantArray(); + } + + public function isOversizedArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isOversizedArray(); + } + + public function isList(): TrinaryLogic + { + return $this->getStaticObjectType()->isList(); + } + + public function isNull(): TrinaryLogic + { + return $this->getStaticObjectType()->isNull(); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantValue(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantScalarValue(); + } + + public function getConstantScalarTypes(): array + { + return $this->getStaticObjectType()->getConstantScalarTypes(); + } + + public function getConstantScalarValues(): array + { + return $this->getStaticObjectType()->getConstantScalarValues(); + } + + public function isTrue(): TrinaryLogic + { + return $this->getStaticObjectType()->isTrue(); + } + + public function isFalse(): TrinaryLogic + { + return $this->getStaticObjectType()->isFalse(); + } + + public function isBoolean(): TrinaryLogic + { + return $this->getStaticObjectType()->isBoolean(); + } + + public function isFloat(): TrinaryLogic + { + return $this->getStaticObjectType()->isFloat(); + } + + public function isInteger(): TrinaryLogic + { + return $this->getStaticObjectType()->isInteger(); + } + + public function isString(): TrinaryLogic + { + return $this->getStaticObjectType()->isString(); + } + public function isNumericString(): TrinaryLogic { return $this->getStaticObjectType()->isNumericString(); @@ -328,15 +624,56 @@ public function isNonEmptyString(): TrinaryLogic return $this->getStaticObjectType()->isNonEmptyString(); } + public function isNonFalsyString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNonFalsyString(); + } + public function isLiteralString(): TrinaryLogic { return $this->getStaticObjectType()->isLiteralString(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ + public function isLowercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isLowercaseString(); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isUppercaseString(); + } + + public function isClassString(): TrinaryLogic + { + return $this->getStaticObjectType()->isClassString(); + } + + public function getClassStringObjectType(): Type + { + return $this->getStaticObjectType()->getClassStringObjectType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return $this->getStaticObjectType()->isVoid(); + } + + public function isScalar(): TrinaryLogic + { + return $this->getStaticObjectType()->isScalar(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return $this->getStaticObjectType()->getCallableParametersAcceptors($scope); @@ -352,6 +689,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return $this->getStaticObjectType()->toString(); @@ -372,6 +714,16 @@ public function toArray(): Type return $this->getStaticObjectType()->toArray(); } + public function toArrayKey(): Type + { + return $this->getStaticObjectType()->toArrayKey(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->getStaticObjectType()->toCoercedArgumentType($strictTypes); + } + public function toBoolean(): BooleanType { return $this->getStaticObjectType()->toBoolean(); @@ -379,21 +731,89 @@ public function toBoolean(): BooleanType public function traverse(callable $cb): Type { + $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null; + + if ($subtractedType !== $this->subtractedType) { + return new self( + $this->classReflection, + $subtractedType, + ); + } + return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass'])); + if ($this->subtractedType === null) { + return $this; } - return new ErrorType(); + 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 IdentifierTypeNode('static'); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index ba99a36130..93fe12d555 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -8,7 +8,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -class StaticTypeFactory +final class StaticTypeFactory { public static function falsey(): Type @@ -35,7 +35,7 @@ public static function truthy(): Type static $truthy; if ($truthy === null) { - $truthy = new MixedType(false, self::falsey()); + $truthy = new MixedType(subtractedType: self::falsey()); } return $truthy; diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 03846473ed..71678b77a4 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -2,48 +2,87 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonArrayTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonIterableTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; class StrictMixedType implements CompoundType { use UndecidedComparisonCompoundTypeTrait; + use NonArrayTypeTrait; + use NonIterableTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - return TrinaryLogic::createYes(); + return []; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function getObjectClassReflections(): array { - return TrinaryLogic::createFromBoolean( - $acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType - ); + return []; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function getConstantStrings(): array { - return TrinaryLogic::createFromBoolean($type instanceof self); + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + if ($acceptingType instanceof self) { + return AcceptsResult::createYes(); + } + if ($acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType) { + return AcceptsResult::createYes(); + } + + return AcceptsResult::createMaybe(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return TrinaryLogic::createFromBoolean($otherType instanceof self); + return IsSuperTypeOfResult::createYes(); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof self) { + return IsSuperTypeOfResult::createYes(); + } + if ($otherType instanceof MixedType && !$otherType instanceof TemplateMixedType) { + return IsSuperTypeOfResult::createYes(); + } + + return IsSuperTypeOfResult::createMaybe(); } public function equals(Type $type): bool @@ -53,7 +92,27 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - return 'mixed'; + return $level->handle( + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'strict-mixed', + ); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new ErrorType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function canAccessProperties(): TrinaryLogic @@ -66,14 +125,44 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -86,14 +175,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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -106,9 +195,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 @@ -131,7 +220,57 @@ 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(); } @@ -146,16 +285,66 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); @@ -171,6 +360,16 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -196,6 +395,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -216,6 +420,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(); @@ -226,18 +440,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 8f08f6e9be..feb18e97d6 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -3,26 +3,54 @@ namespace PHPStan\Type; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\TrinaryLogic; class StringAlwaysAcceptingObjectWithToStringType extends StringType { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof TypeWithClassName) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::isSuperTypeOf($type); + } + + $result = IsSuperTypeOfResult::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { + return IsSuperTypeOfResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(IsSuperTypeOfResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); + } + + return $result; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::accepts($type, $strictTypes); + } + + $result = AcceptsResult::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { + return AcceptsResult::createNo(); } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString') - ); + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(AcceptsResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); } - return parent::accepts($type, $strictTypes); + return $result; } } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 52ebc07712..44fa96ab15 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,16 +2,26 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function count; /** @api */ class StringType implements Type @@ -19,11 +29,13 @@ 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() @@ -35,14 +47,24 @@ public function describe(VerbosityLevel $level): string return 'string'; } + public function getConstantStrings(): array + { + return []; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -51,7 +73,10 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -65,36 +90,54 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } - if ((new IntegerType())->isSuperTypeOf($offsetType)->yes() || $offsetType instanceof MixedType) { - return new StringType(); + if ($offsetType->isInteger()->yes() || $offsetType instanceof MixedType) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } return new ErrorType(); } - public function 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 $type->isAcceptedBy($this, $strictTypes); } - if ($type instanceof TypeWithClassName && !$strictTypes) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); - } + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString') - ); + if ($thatClassNames === [] || $strictTypes) { + return AcceptsResult::createNo(); } - return TrinaryLogic::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($thatClassNames[0])) { + return AcceptsResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassNames[0]); + return AcceptsResult::createFromBoolean( + $typeClass->hasNativeMethod('__toString'), + ); } public function toNumber(): Type @@ -102,6 +145,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); @@ -122,10 +170,63 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + isList: TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + if ($this->isNumericString()->no()) { + return TypeCombinator::union($this, $this->toBoolean()); + } + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -136,18 +237,89 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::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/SubtractableType.php b/src/Type/SubtractableType.php index c40188af2d..931fe0bc4c 100644 --- a/src/Type/SubtractableType.php +++ b/src/Type/SubtractableType.php @@ -5,8 +5,6 @@ interface SubtractableType extends Type { - public function subtract(Type $type): Type; - public function getTypeWithoutSubtractedType(): Type; public function changeSubtractedType(?Type $subtractedType): Type; diff --git a/src/Type/ThisType.php b/src/Type/ThisType.php index ff5dcef89d..39d4949ca8 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -2,8 +2,10 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use function sprintf; /** @api */ class ThisType extends StaticType @@ -12,33 +14,75 @@ class ThisType extends StaticType /** * @api */ - public function __construct(ClassReflection $classReflection) + public function __construct( + ClassReflection $classReflection, + ?Type $subtractedType = null, + ) { - parent::__construct($classReflection); + parent::__construct($classReflection, $subtractedType); } public function changeBaseClass(ClassReflection $classReflection): StaticType { - return new self($classReflection); + return new self($classReflection, $this->getSubtractedType()); } public function describe(VerbosityLevel $level): string { - return sprintf('$this(%s)', $this->getClassName()); + return sprintf('$this(%s)', $this->getStaticObjectType()->describe($level)); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $parent = new parent($this->getClassReflection(), $this->getSubtractedType()); + + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + $type = parent::changeSubtractedType($subtractedType); + if ($type instanceof parent) { + return new self($type->getClassReflection(), $subtractedType); + } + + return $type; + } + + public function traverse(callable $cb): Type + { + $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; + + if ($subtractedType !== $this->getSubtractedType()) { + return new self( + $this->getClassReflection(), + $subtractedType, + ); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass'])); + if ($this->getSubtractedType() === null) { + return $this; } - return new ErrorType(); + return new self($this->getClassReflection()); + } + + public function toPhpDocNode(): TypeNode + { + return new ThisTypeNode(); } } diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php new file mode 100644 index 0000000000..813b0136d9 --- /dev/null +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -0,0 +1,211 @@ +yes() + ? $this + : TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getIterableValueType()), new AccessoryArrayListType()); + $chunkType = TypeCombinator::intersect($chunkType, new NonEmptyArrayType()); + + $arrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $chunkType), new AccessoryArrayListType()); + + return $this->isIterableAtLeastOnce()->yes() + ? TypeCombinator::intersect($arrayType, new NonEmptyArrayType()) + : $arrayType; + } + +} diff --git a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php index 44cac2d98e..c6efe96950 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -2,7 +2,9 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; @@ -12,7 +14,7 @@ trait ConstantNumericComparisonTypeTrait { - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -22,15 +24,17 @@ public function getSmallerType(): Type if (!(bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllGreaterThan($this->value), + // subtract range when we support float-ranges ]; if (!(bool) $this->value) { @@ -40,11 +44,12 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), + new ConstantFloatType(0.0), // subtract range when we support float-ranges IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), ]; @@ -55,7 +60,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllSmallerThan($this->value), @@ -64,6 +69,7 @@ public function getGreaterOrEqualType(): Type if ((bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index 44e8674c39..f458512622 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -2,43 +2,71 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\Type; trait ConstantScalarTypeTrait { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean($this->value === $type->value); + return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return parent::accepts($type, $strictTypes)->and(AcceptsResult::createMaybe()); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if (!$this instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.notAllowed, equal.invalid, equal.alwaysFalse + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + if ($type instanceof CompoundType) { + return $type->looseCompare($this, $phpVersion); + } + + return parent::looseCompare($type, $phpVersion); } public function equals(Type $type): bool @@ -46,35 +74,55 @@ public function equals(Type $type): bool return $type instanceof self && $this->value === $type->value; } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean($this->value < $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThan($this); + return $otherType->isGreaterThan($this, $phpVersion); } return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean($this->value <= $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThanOrEqual($this); + return $otherType->isGreaterThanOrEqual($this, $phpVersion); } return TrinaryLogic::createMaybe(); } - public function generalize(GeneralizePrecision $precision): Type + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function getFiniteTypes(): array { - return new parent(); + return [$this]; } } diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php new file mode 100644 index 0000000000..6377fb21eb --- /dev/null +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -0,0 +1,632 @@ +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 hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->resolve()->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->resolve()->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->resolve()->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->resolve()->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->resolve()->getStaticProperty($propertyName, $scope); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->resolve()->getUnresolvedStaticPropertyPrototype($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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->resolve()->getKeysArrayFiltered($filterValueType, $strict); + } + + 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, ?TrinaryLogic $strict = null): Type + { + return $this->resolve()->searchArray($needleType, $strict); + } + + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->resolve()->spliceArray($offsetType, $lengthType, $replacementType); + } + + 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 + { + return $this->result ??= $this->getResult(); + } + + 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..a4080f0aa1 --- /dev/null +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -0,0 +1,112 @@ +getKeysArray(); + } + + public function getKeysArray(): Type + { + return new ErrorType(); + } + + public function getValuesArray(): Type + { + return new ErrorType(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new ErrorType(); + } + + public function flipArray(): Type + { + return new ErrorType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new ErrorType(); + } + + public function popArray(): Type + { + return new ErrorType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + return new ErrorType(); + } + + public function shiftArray(): Type + { + return new ErrorType(); + } + + public function shuffleArray(): Type + { + return new ErrorType(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return new ErrorType(); + } + +} diff --git a/src/Type/Traits/MaybeCallableTypeTrait.php b/src/Type/Traits/MaybeCallableTypeTrait.php index 69bba1af5f..cc22cac99b 100644 --- a/src/Type/Traits/MaybeCallableTypeTrait.php +++ b/src/Type/Traits/MaybeCallableTypeTrait.php @@ -14,10 +14,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; diff --git a/src/Type/Traits/MaybeIterableTypeTrait.php b/src/Type/Traits/MaybeIterableTypeTrait.php index 16efedf276..48eae13646 100644 --- a/src/Type/Traits/MaybeIterableTypeTrait.php +++ b/src/Type/Traits/MaybeIterableTypeTrait.php @@ -3,6 +3,8 @@ namespace PHPStan\Type\Traits; use PHPStan\TrinaryLogic; +use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -19,14 +21,47 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + if ($this->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 5ed46f4aeb..71e4df3421 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -2,23 +2,39 @@ namespace PHPStan\Type\Traits; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\Dummy\DummyConstantReflection; +use PHPStan\Reflection\Dummy\DummyClassConstantReflection; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; trait MaybeObjectTypeTrait { + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new MixedType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -29,21 +45,61 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -57,7 +113,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -69,9 +125,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -85,9 +139,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 755bf8c44e..c0f0f44ea2 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -14,6 +14,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -29,4 +34,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return $this; + } + } diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php new file mode 100644 index 0000000000..5f586ad1a6 --- /dev/null +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -0,0 +1,112 @@ +getKeysArray(); + } + + public function getKeysArray(): Type + { + return new ErrorType(); + } + + public function getValuesArray(): Type + { + return new ErrorType(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new ErrorType(); + } + + public function flipArray(): Type + { + return new ErrorType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new ErrorType(); + } + + public function popArray(): Type + { + return new ErrorType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + return new ErrorType(); + } + + public function shiftArray(): Type + { + return new ErrorType(); + } + + public function shuffleArray(): Type + { + return new ErrorType(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return new ErrorType(); + } + +} diff --git a/src/Type/Traits/NonCallableTypeTrait.php b/src/Type/Traits/NonCallableTypeTrait.php index 75a6233d4c..e64719b1d1 100644 --- a/src/Type/Traits/NonCallableTypeTrait.php +++ b/src/Type/Traits/NonCallableTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Traits; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; trait NonCallableTypeTrait @@ -15,7 +16,7 @@ public function isCallable(): TrinaryLogic public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Type/Traits/NonGeneralizableTypeTrait.php b/src/Type/Traits/NonGeneralizableTypeTrait.php new file mode 100644 index 0000000000..e943051c95 --- /dev/null +++ b/src/Type/Traits/NonGeneralizableTypeTrait.php @@ -0,0 +1,16 @@ +traverse(static fn (Type $type) => $type->generalize($precision)); + } + +} diff --git a/src/Type/Traits/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 b4ea902759..0d55341b46 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -2,17 +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(); @@ -23,14 +36,44 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -43,14 +86,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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -63,9 +106,14 @@ 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 getConstantStrings(): array + { + return []; } public function isCloneable(): TrinaryLogic @@ -73,4 +121,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 68a703d069..d74dc8d0d1 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -26,7 +26,17 @@ public function getOffsetValueType(Type $offsetType): 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(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, + ); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, ); } @@ -66,7 +124,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -78,9 +136,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -94,9 +150,14 @@ 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 getConstantStrings(): array + { + return []; } public function isCloneable(): TrinaryLogic @@ -104,7 +165,57 @@ 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(); } @@ -119,19 +230,69 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { - return new StringType(); + return new ErrorType(); } public function toInteger(): Type @@ -149,4 +310,18 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + } diff --git a/src/Type/Traits/SubstractableTypeTrait.php b/src/Type/Traits/SubstractableTypeTrait.php new file mode 100644 index 0000000000..bf563eb899 --- /dev/null +++ b/src/Type/Traits/SubstractableTypeTrait.php @@ -0,0 +1,30 @@ +getSubtractedType() !== null) + ) { + return sprintf('~(%s)', $subtractedType->describe($level)); + } + + return sprintf('~%s', $subtractedType->describe($level)); + } + +} diff --git a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php index e40b72fad4..afec40a13e 100644 --- a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; @@ -10,13 +11,21 @@ trait UndecidedComparisonCompoundTypeTrait use UndecidedComparisonTypeTrait; - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes() && $this->isObject()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } diff --git a/src/Type/Traits/UndecidedComparisonTypeTrait.php b/src/Type/Traits/UndecidedComparisonTypeTrait.php index e5c6d2c891..489aee2f6e 100644 --- a/src/Type/Traits/UndecidedComparisonTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonTypeTrait.php @@ -2,40 +2,54 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; trait UndecidedComparisonTypeTrait { - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes() && $this->isObject()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { return new MixedType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { - return new MixedType(); + return new MixedType(subtractedType: new NullType()); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { + if ($this->isObject()->yes()) { + return new MixedType(subtractedType: new NullType()); + } + return new MixedType(); } diff --git a/src/Type/Type.php b/src/Type/Type.php index 099b75d5ae..19e474f9e3 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -2,29 +2,71 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; -/** @api */ +/** + * @api + * @see https://phpstan.org/developing-extensions/type-system + */ interface Type { /** - * @return string[] + * @return list */ public function getReferencedClasses(): array; - public function accepts(Type $type, bool $strictTypes): TrinaryLogic; + /** @return list */ + public function getObjectClassNames(): array; - public function isSuperTypeOf(Type $type): TrinaryLogic; + /** + * @return list + */ + public function getObjectClassReflections(): array; + + /** + * Returns object type Foo for class-string and 'Foo' (if Foo is a valid class). + */ + public function getClassStringObjectType(): Type; + + /** + * Returns object type Foo for class-string, 'Foo' (if Foo is a valid class), + * and object type Foo. + */ + public function getObjectTypeOrClassStringObjectType(): Type; + + public function isObject(): TrinaryLogic; + + public function isEnum(): TrinaryLogic; + + /** @return list */ + public function getArrays(): array; + + /** @return list */ + public function getConstantArrays(): array; + + /** @return list */ + public function getConstantStrings(): array; + + public function accepts(Type $type, bool $strictTypes): AcceptsResult; + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; public function equals(Type $type): bool; @@ -32,17 +74,32 @@ public function describe(VerbosityLevel $level): string; public function canAccessProperties(): TrinaryLogic; + /** @deprecated Use hasInstanceProperty or hasStaticProperty instead */ public function hasProperty(string $propertyName): TrinaryLogic; - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection; + /** @deprecated Use getInstanceProperty or getStaticProperty instead */ + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + /** @deprecated Use getUnresolvedInstancePropertyPrototype or getUnresolvedStaticPropertyPrototype instead */ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; + public function hasInstanceProperty(string $propertyName): TrinaryLogic; + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; + + public function hasStaticProperty(string $propertyName): TrinaryLogic; + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + + public function getUnresolvedStaticPropertyPrototype(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; @@ -50,31 +107,103 @@ public function canAccessConstants(): TrinaryLogic; public function hasConstant(string $constantName): TrinaryLogic; - public function getConstant(string $constantName): ConstantReflection; + public function getConstant(string $constantName): ClassConstantReflection; public function isIterable(): TrinaryLogic; public function isIterableAtLeastOnce(): TrinaryLogic; + public function getArraySize(): Type; + public function getIterableKeyType(): Type; + public function getFirstIterableKeyType(): Type; + + public function getLastIterableKeyType(): Type; + public function getIterableValueType(): Type; + public function getFirstIterableValueType(): Type; + + public function getLastIterableValueType(): Type; + public function isArray(): TrinaryLogic; + public function isConstantArray(): TrinaryLogic; + + public function isOversizedArray(): TrinaryLogic; + + public function isList(): TrinaryLogic; + public function isOffsetAccessible(): TrinaryLogic; + public function isOffsetAccessLegal(): TrinaryLogic; + public function hasOffsetValueType(Type $offsetType): TrinaryLogic; public function getOffsetValueType(Type $offsetType): Type; public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type; + + public function unsetOffset(Type $offsetType): Type; + + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): 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, ?TrinaryLogic $strict = null): Type; + + public function shiftArray(): Type; + + public function shuffleArray(): Type; + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; + + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): 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; @@ -92,23 +221,100 @@ public function toString(): Type; public function toArray(): Type; - public function isSmallerThan(Type $otherType): TrinaryLogic; + 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. + */ + 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 isSmallerThanOrEqual(Type $otherType): 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 getSmallerType(): Type; + public function isLowercaseString(): TrinaryLogic; + + public function isUppercaseString(): TrinaryLogic; + + public function isClassString(): TrinaryLogic; + + public function isVoid(): TrinaryLogic; - public function getSmallerOrEqualType(): Type; + public function isScalar(): TrinaryLogic; - public function getGreaterType(): Type; + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; - public function getGreaterOrEqualType(): Type; + 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 @@ -132,10 +338,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 * @@ -147,9 +355,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 index 578a3b2c49..17bd6373e5 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -7,22 +7,16 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -class TypeAlias +final class TypeAlias { - private TypeNode $typeNode; - - private NameScope $nameScope; - private ?Type $resolvedType = null; public function __construct( - TypeNode $typeNode, - NameScope $nameScope + private TypeNode $typeNode, + private NameScope $nameScope, ) { - $this->typeNode = $typeNode; - $this->nameScope = $nameScope; } public static function invalid(): self @@ -34,14 +28,10 @@ public static function invalid(): self public function resolve(TypeNodeResolver $typeNodeResolver): Type { - if ($this->resolvedType === null) { - $this->resolvedType = $typeNodeResolver->resolve( - $this->typeNode, - $this->nameScope - ); - } - - return $this->resolvedType; + return $this->resolvedType ??= $typeNodeResolver->resolve( + $this->typeNode, + $this->nameScope, + ); } } diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 3373f8d0b8..af6be470cc 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -3,162 +3,12 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; -use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDoc\TypeStringResolver; -use PHPStan\Reflection\ReflectionProvider; -use function array_key_exists; -class TypeAliasResolver +interface TypeAliasResolver { - /** @var array */ - private array $globalTypeAliases; + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool; - private TypeStringResolver $typeStringResolver; - - private TypeNodeResolver $typeNodeResolver; - - private ReflectionProvider $reflectionProvider; - - /** @var array */ - private array $resolvedGlobalTypeAliases = []; - - /** @var array */ - private array $resolvedLocalTypeAliases = []; - - /** @var array */ - private array $resolvingClassTypeAliases = []; - - /** @var array */ - private array $inProcess = []; - - /** - * @param array $globalTypeAliases - */ - public function __construct( - array $globalTypeAliases, - TypeStringResolver $typeStringResolver, - TypeNodeResolver $typeNodeResolver, - ReflectionProvider $reflectionProvider - ) - { - $this->globalTypeAliases = $globalTypeAliases; - $this->typeStringResolver = $typeStringResolver; - $this->typeNodeResolver = $typeNodeResolver; - $this->reflectionProvider = $reflectionProvider; - } - - public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool - { - $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases); - if ($hasGlobalTypeAlias) { - return true; - } - - if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) { - return false; - } - - $classReflection = $this->reflectionProvider->getClass($classNameScope); - $localTypeAliases = $classReflection->getTypeAliases(); - return array_key_exists($aliasName, $localTypeAliases); - } - - public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - return $this->resolveLocalTypeAlias($aliasName, $nameScope) - ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope); - } - - private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - return null; - } - - if (!$nameScope->hasTypeAlias($aliasName)) { - return null; - } - - $className = $nameScope->getClassName(); - if ($className === null) { - return null; - } - - $aliasNameInClassScope = $className . '::' . $aliasName; - - if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { - return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; - } - - // prevent infinite recursion - if (array_key_exists($className, $this->resolvingClassTypeAliases)) { - return null; - } - - $this->resolvingClassTypeAliases[$className] = true; - - if (!$this->reflectionProvider->hasClass($className)) { - unset($this->resolvingClassTypeAliases[$className]); - return null; - } - - $classReflection = $this->reflectionProvider->getClass($className); - $localTypeAliases = $classReflection->getTypeAliases(); - - unset($this->resolvingClassTypeAliases[$className]); - - if (!array_key_exists($aliasName, $localTypeAliases)) { - return null; - } - - if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { - // resolve circular reference as ErrorType to make it easier to detect - throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); - } - - $this->inProcess[$aliasNameInClassScope] = true; - - try { - $unresolvedAlias = $localTypeAliases[$aliasName]; - $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); - } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { - $resolvedAliasType = new 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 \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); - } - - if (array_key_exists($aliasName, $this->inProcess)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); - } - - $this->inProcess[$aliasName] = true; - - $aliasTypeString = $this->globalTypeAliases[$aliasName]; - $aliasType = $this->typeStringResolver->resolve($aliasTypeString); - $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; - - unset($this->inProcess[$aliasName]); - - return $aliasType; - } + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type; } diff --git a/src/Type/TypeAliasResolverProvider.php b/src/Type/TypeAliasResolverProvider.php new file mode 100644 index 0000000000..a0a57ef1ac --- /dev/null +++ b/src/Type/TypeAliasResolverProvider.php @@ -0,0 +1,10 @@ +isSuperTypeOf($type)->no()) { + return self::union($type, $nullType); + } + + return $type; } public static function remove(Type $fromType, Type $typeToRemove): Type @@ -37,15 +67,6 @@ public static function remove(Type $fromType, Type $typeToRemove): Type return $fromType; } - if ($fromType instanceof UnionType) { - $innerTypes = []; - foreach ($fromType->getTypes() as $innerType) { - $innerTypes[] = self::remove($innerType, $typeToRemove); - } - - return self::union(...$innerTypes); - } - $isSuperType = $typeToRemove->isSuperTypeOf($fromType); if ($isSuperType->yes()) { return new NeverType(); @@ -61,81 +82,37 @@ public static function remove(Type $fromType, Type $typeToRemove): Type } } - if ($fromType instanceof BooleanType) { - if ($typeToRemove instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$typeToRemove->getValue()); - } - } elseif ($fromType instanceof IterableType) { - $arrayType = new ArrayType(new MixedType(), new MixedType()); - if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return new GenericObjectType(\Traversable::class, [ - $fromType->getIterableKeyType(), - $fromType->getIterableValueType(), - ]); - } + $removed = $fromType->tryRemove($typeToRemove); + if ($removed !== null) { + return $removed; + } - $traversableType = new ObjectType(\Traversable::class); - if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return new ArrayType($fromType->getIterableKeyType(), $fromType->getIterableValueType()); - } - } elseif ($fromType instanceof IntegerRangeType) { - $type = $fromType->tryRemove($typeToRemove); - if ($type !== null) { - return $type; - } - } elseif ($fromType instanceof IntegerType) { - if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { - if ($typeToRemove instanceof IntegerRangeType) { - $removeValueMin = $typeToRemove->getMin(); - $removeValueMax = $typeToRemove->getMax(); - } else { - $removeValueMin = $typeToRemove->getValue(); - $removeValueMax = $typeToRemove->getValue(); - } - $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; - $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; - if ($lowerPart !== null && $upperPart !== null) { - return self::union($lowerPart, $upperPart); - } - return $lowerPart ?? $upperPart ?? new NeverType(); - } - } elseif ($fromType->isArray()->yes()) { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { - return self::intersect($fromType, new NonEmptyArrayType()); - } + $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; + } - if ($typeToRemove instanceof NonEmptyArrayType) { - return new ConstantArrayType([], []); - } + $result[] = $finiteType; + } - if ($fromType instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { - return $fromType->unsetOffset($typeToRemove->getOffsetType()); - } - } elseif ($fromType instanceof StringType) { - if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '') { - return self::intersect($fromType, new AccessoryNonEmptyStringType()); - } - if ($typeToRemove instanceof AccessoryNonEmptyStringType) { - return new ConstantStringType(''); - } - } elseif ($fromType instanceof ObjectType && $fromType->getClassName() === \DateTimeInterface::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === \DateTimeImmutable::class) { - return new ObjectType(\DateTime::class); - } + if (count($result) === count($fromFiniteTypes)) { + return $fromType; + } - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === \DateTime::class) { - return new ObjectType(\DateTimeImmutable::class); - } - } + if (count($result) === 0) { + return new NeverType(); + } - if ($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); } } @@ -168,18 +145,27 @@ 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) { if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) { $benevolentUnionObject = $types[$i]; } - foreach ($types[$i]->getTypes() as $benevolentInnerType) { + $benevolentTypesCount = 0; + $typesInner = $types[$i]->getTypes(); + foreach ($typesInner as $benevolentInnerType) { + $benevolentTypesCount++; $benevolentTypes[$benevolentInnerType->describe(VerbosityLevel::value())] = $benevolentInnerType; } - array_splice($types, $i, 1, $types[$i]->getTypes()); + array_splice($types, $i, 1, $typesInner); + $typesCount += $benevolentTypesCount - 1; continue; } if (!($types[$i] instanceof UnionType)) { @@ -189,64 +175,59 @@ public static function union(Type ...$types): Type 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) { + if ($types[$i]->isConstantScalarValue()->yes()) { $type = $types[$i]; $scalarTypes[get_class($type)][md5($type->describe(VerbosityLevel::cache()))] = $type; unset($types[$i]); continue; } - if ($types[$i] instanceof BooleanType) { + if ($types[$i]->isBoolean()->yes()) { $hasGenericScalarTypes[ConstantBooleanType::class] = true; } - if ($types[$i] instanceof FloatType) { + if ($types[$i]->isFloat()->yes()) { $hasGenericScalarTypes[ConstantFloatType::class] = true; } - if ($types[$i] instanceof IntegerType && !$types[$i] instanceof IntegerRangeType) { + if ($types[$i]->isInteger()->yes() && !$types[$i] instanceof IntegerRangeType) { $hasGenericScalarTypes[ConstantIntegerType::class] = true; } - if ($types[$i] instanceof StringType && !$types[$i] instanceof ClassStringType) { + if ($types[$i]->isString()->yes() && $types[$i]->isClassString()->no() && TypeUtils::getAccessoryTypes($types[$i]) === []) { $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]); } @@ -254,36 +235,15 @@ public static function union(Type ...$types): Type $scalarTypes[$classType] = array_values($scalarTypeItems); } - /** @var ArrayType[] $arrayTypes */ - $arrayTypes = $arrayTypes; - - $arrayAccessoryTypesToProcess = []; - if (count($arrayAccessoryTypes) > 1) { - $arrayAccessoryTypesToProcess = array_values(array_intersect_key(...$arrayAccessoryTypes)); - } elseif (count($arrayAccessoryTypes) > 0) { - $arrayAccessoryTypesToProcess = array_values($arrayAccessoryTypes[0]); - } - - $types = array_values( - array_merge( - $types, - self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess) - ) + $enumCaseTypes = array_values($enumCaseTypes); + usort( + $integerRangeTypes, + static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN) + ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX), ); - - // 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])) { @@ -292,12 +252,14 @@ public static function union(Type ...$types): Type } if ($classType === ConstantBooleanType::class && count($scalarTypeItems) === 2) { $types[] = new BooleanType(); + $typesCount++; unset($scalarTypes[$classType]); continue; } - for ($i = 0; $i < count($types); $i++) { - for ($j = 0; $j < count($scalarTypeItems); $j++) { + $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; @@ -307,11 +269,13 @@ public static function union(Type ...$types): Type 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; } } @@ -320,10 +284,24 @@ public static function union(Type ...$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++) { + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { $compareResult = self::compareTypesInUnion($types[$i], $types[$j]); if ($compareResult === null) { continue; @@ -333,30 +311,62 @@ public static function union(Type ...$types): Type 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; + } + } + } + + $enumCasesCount = count($enumCaseTypes); + for ($i = 0; $i < $typesCount; $i++) { + for ($j = 0; $j < $enumCasesCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $enumCaseTypes[$j]); + if ($compareResult === null) { + continue; + } + + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($enumCaseTypes, $j--, 1); + $enumCasesCount--; + continue 1; + } + if ($b !== null) { + $enumCaseTypes[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; continue 2; } } } + foreach ($enumCaseTypes as $enumCaseType) { + $types[] = $enumCaseType; + $typesCount++; + } + foreach ($scalarTypes as $scalarTypeItems) { foreach ($scalarTypeItems as $scalarType) { $types[] = $scalarType; + $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())])) { @@ -366,21 +376,19 @@ public static function union(Type ...$types): Type unset($tempTypes[$i]); } - if (count($tempTypes) === 0) { + if ($tempTypes === []) { if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) { return $benevolentUnionObject->withTypes($types); } - return new BenevolentUnionType($types); + return new BenevolentUnionType($types, true); } } - return new UnionType($types); + return new UnionType($types, true); } /** - * @param Type $a - * @param Type $b * @return array{Type, null}|array{null, Type}|null */ private static function compareTypesInUnion(Type $a, Type $b): ?array @@ -399,6 +407,28 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array 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(); @@ -408,11 +438,7 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b); } if ($isSuperType->yes()) { - $subtractedType = null; - if ($b instanceof SubtractableType) { - $subtractedType = $b->getSubtractedType(); - } - $a = self::intersectWithSubtractedType($a, $subtractedType); + $a = self::intersectWithSubtractedType($a, $b); return [$a, null]; } } @@ -425,65 +451,115 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a); } if ($isSuperType->yes()) { - $subtractedType = null; - if ($a instanceof SubtractableType) { - $subtractedType = $a->getSubtractedType(); - } - $b = self::intersectWithSubtractedType($b, $subtractedType); + $b = self::intersectWithSubtractedType($b, $a); return [null, $b]; } } - if ( - !$b instanceof ConstantArrayType - && $b->isSuperTypeOf($a)->yes() - ) { + if ($b->isSuperTypeOf($a)->yes()) { return [null, $b]; } - if ( - !$a instanceof ConstantArrayType - && $a->isSuperTypeOf($b)->yes() - ) { + if ($a->isSuperTypeOf($b)->yes()) { return [$a, null]; } if ( $a instanceof ConstantStringType && $a->getValue() === '' - && $b->describe(VerbosityLevel::value()) === 'non-empty-string' + && ($b->describe(VerbosityLevel::value()) === 'non-empty-string' + || $b->describe(VerbosityLevel::value()) === 'non-falsy-string') ) { - return [null, new StringType()]; + return [null, self::intersect( + new StringType(), + ...self::getAccessoryCaseStringTypes($b), + )]; } if ( $b instanceof ConstantStringType && $b->getValue() === '' - && $a->describe(VerbosityLevel::value()) === 'non-empty-string' + && ($a->describe(VerbosityLevel::value()) === 'non-empty-string' + || $a->describe(VerbosityLevel::value()) === 'non-falsy-string') ) { - return [new StringType(), null]; + 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) { return $type; } - if ($type instanceof SubtractableType) { - if ($type->getSubtractedType() === null) { - return $type; + if ($subtractedType instanceof SubtractableType) { + $withoutSubtracted = $subtractedType->getTypeWithoutSubtractedType(); + if ($withoutSubtracted->isSuperTypeOf($type)->yes()) { + $subtractedSubtractedType = $subtractedType->getSubtractedType(); + if ($subtractedSubtractedType === null) { + return new NeverType(); + } + + return self::intersect($type, $subtractedSubtractedType); } + } + + if ($type instanceof SubtractableType) { + $subtractedType = $type->getSubtractedType() === null + ? $subtractedType + : self::union($type->getSubtractedType(), $subtractedType); - $subtractedType = self::union( - $type->getSubtractedType(), - $subtractedType + $subtractedType = self::intersect( + $type->getTypeWithoutSubtractedType(), + $subtractedType, ); if ($subtractedType instanceof NeverType) { $subtractedType = null; @@ -500,58 +576,176 @@ private static function unionWithSubtractedType( } private static function intersectWithSubtractedType( - SubtractableType $subtractableType, - ?Type $subtractedType + SubtractableType $a, + Type $b, ): Type { - if ($subtractableType->getSubtractedType() === null) { - return $subtractableType; + if ($a->getSubtractedType() === null) { + return $a; } - if ($subtractedType === null) { - return $subtractableType->getTypeWithoutSubtractedType(); + if ($b instanceof 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(subtractedType: $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 ($constantArrays as $constantArray) { + if ($constantArray->isList()->yes()) { + $list = new AccessoryArrayListType(); + $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list; + } - foreach ($accessoryTypes as $i => $accessoryType) { - if (!$accessoryType instanceof NonEmptyArrayType) { + if (!$constantArray->isIterableAtLeastOnce()->yes()) { continue; } - unset($accessoryTypes[$i]); - break 2; + $nonEmpty = new NonEmptyArrayType(); + $accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty; + } + } + + $commonAccessoryTypes = []; + $arrayTypeCount = count($arrayTypes); + foreach ($accessoryTypes as $accessoryType) { + if (count($accessoryType) !== $arrayTypeCount) { + $firstKey = array_key_first($accessoryType); + if ($accessoryType[$firstKey] instanceof OversizedArrayType) { + $commonAccessoryTypes[] = $accessoryType[$firstKey]; + } + continue; + } + + if ($accessoryType[0] instanceof HasOffsetValueType) { + $commonAccessoryTypes[] = self::union(...$accessoryType); + continue; } + + $commonAccessoryTypes[] = $accessoryType[0]; + } + + if (TrinaryLogic::createYes()->and(...$isIterableAtLeastOnce)->yes()) { + $commonAccessoryTypes[] = new NonEmptyArrayType(); } - if (count($arrayTypes) === 0) { + + 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), ]; } @@ -559,125 +753,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; + } + + $keyType = self::union(...$keyTypes); + $valueType = self::union(...$valueTypes); + + if ($valueType instanceof UnionType && count($valueType->getTypes()) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $valueType = $valueType->generalize(GeneralizePrecision::lessSpecific()); } - $resultArrays[] = self::intersect($builder->getArray(), ...$accessoryTypes); + $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; + } + } + + $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]]++; + } + } } - for ($i = 0; $i < count($arraysToProcess); $i++) { - for ($j = $i + 1; $j < count($arraysToProcess); $j++) { - if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])) { + 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; + } } } @@ -686,6 +1071,16 @@ 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; @@ -717,15 +1112,21 @@ public static function intersect(Type ...$types): Type $topLevelUnionSubTypes = []; $innerTypes = $type->getTypes(); usort($innerTypes, $sortTypes); + $slice1 = array_slice($types, 0, $i); + $slice2 = array_slice($types, $i + 1); foreach ($innerTypes as $innerUnionSubType) { $topLevelUnionSubTypes[] = self::intersect( $innerUnionSubType, - ...array_slice($types, 0, $i), - ...array_slice($types, $i + 1) + ...$slice1, + ...$slice2, ); } $union = self::union(...$topLevelUnionSubTypes); + if ($union instanceof NeverType) { + return $union; + } + if ($type instanceof BenevolentUnionType) { $union = TypeUtils::toBenevolentUnion($union); } @@ -735,26 +1136,64 @@ public static function intersect(Type ...$types): Type $type->getScope(), $type->getName(), $union, - $type->getVariance() + $type->getVariance(), + $type->getStrategy(), + $type->getDefault(), ); - if ($type->isArgument()) { - return TemplateTypeHelper::toArgument($union); - } } return $union; } + $typesCount = count($types); // transform A & (B & C) to A & B & C - for ($i = 0; $i < count($types); $i++) { + for ($i = 0; $i < $typesCount; $i++) { $type = $types[$i]; + if (!($type instanceof IntersectionType)) { continue; } array_splice($types, $i--, 1, $type->getTypes()); + $typesCount = count($types); } + $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 losing 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 @@ -763,8 +1202,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(); @@ -776,6 +1215,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; } } @@ -791,6 +1231,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeSubtractableB->yes()) { $types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType()); array_splice($types, $i--, 1); + $typesCount--; continue 2; } } @@ -800,6 +1241,7 @@ public static function intersect(Type ...$types): Type if ($intersectionType !== null) { $types[$j] = $intersectionType; array_splice($types, $i--, 1); + $typesCount--; continue 2; } } @@ -812,6 +1254,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeA->yes()) { array_splice($types, $j--, 1); + $typesCount--; continue; } @@ -825,20 +1268,131 @@ public static function intersect(Type ...$types): Type if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetType) { $types[$i] = $types[$i]->makeOffsetRequired($types[$j]->getOffsetType()); array_splice($types, $j--, 1); + $typesCount--; continue; } if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetType) { $types[$j] = $types[$j]->makeOffsetRequired($types[$i]->getOffsetType()); array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + 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 IterableType) && - ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) + ($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]->getKeyType(), $types[$j]->getKeyType()); + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType()); $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType()); if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) { $types[$j] = new IterableType($keyType, $itemType); @@ -846,14 +1400,38 @@ public static function intersect(Type ...$types): Type $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; } if ($isSuperTypeB->yes()) { array_splice($types, $i--, 1); + $typesCount--; continue 2; } @@ -863,12 +1441,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 1507e8ce58..a95cf246c1 100644 --- a/src/Type/TypeTraverser.php +++ b/src/Type/TypeTraverser.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class TypeTraverser +final class TypeTraverser { /** @var callable(Type $type, callable(Type): Type $traverse): Type */ diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 1d0dc83c16..d649cfe0a7 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -5,188 +5,42 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantStringType; - -/** @api */ -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[] + * @return list */ - public static function getArrays(Type $type): array + public static function getConstantIntegers(Type $type): array { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); - } - - if ($type instanceof ArrayType) { - return [$type]; - } - - if ($type instanceof UnionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - return []; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - if ($type instanceof IntersectionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - continue; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - return []; + return self::map(ConstantIntegerType::class, $type, false); } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Constant\ConstantArrayType[] + * @return list */ - public static function getConstantArrays(Type $type): array + public static function getIntegerRanges(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, GeneralizePrecision $precision): Type - { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($precision): Type { - if ($type instanceof ConstantType) { - return $type->generalize($precision); - } - - return $traverse($type); - }); + return self::map(IntegerRangeType::class, $type, false); } /** - * @param Type $type - * @return string[] - */ - public static function getDirectClassNames(Type $type): array - { - if ($type instanceof TypeWithClassName) { - return [$type->getClassName()]; - } - - if ($type instanceof UnionType || $type instanceof IntersectionType) { - $classNames = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof TypeWithClassName) { - continue; - } - - $classNames[] = $innerType->getClassName(); - } - - return $classNames; - } - - return []; - } - - /** - * @param Type $type - * @return \PHPStan\Type\ConstantScalarType[] - */ - public static function getConstantScalars(Type $type): array - { - return self::map(ConstantScalarType::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) { @@ -196,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 []; } @@ -204,7 +60,9 @@ private static function map( continue; } - $matchingTypes[] = $innerType; + foreach ($matchingInner as $innerMapped) { + $matchingTypes[] = $innerMapped; + } } return $matchingTypes; @@ -244,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 @@ -290,8 +170,25 @@ public static function findThisType(Type $type): ?ThisType return null; } + public static function findCallableType(Type $type): ?Type + { + if ($type->isCallable()->yes()) { + return $type; + } + + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + $callableType = self::findCallableType($innerType); + if ($callableType !== null) { + return $callableType; + } + } + } + + return null; + } + /** - * @param Type $type * @return HasPropertyType[] */ public static function getHasPropertyTypes(Type $type): array @@ -313,29 +210,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(); } - } - return false; + if ($type instanceof CallableType || $type instanceof ClosureType) { + $ignoreResolveUnresolvableTypesLevel++; + $result = $traverse($type); + $ignoreResolveUnresolvableTypesLevel--; + + return $result; + } + + return $traverse($type); + }); } } diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index 61fc587a6a..076fedcacc 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,111 +2,72 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\Type\Constant\ConstantBooleanType; +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 ReflectionUnionType; +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 'false': - return new ConstantBooleanType(false); - case 'string': - return new StringType(); - case 'float': - return new FloatType(); - case 'array': - return new ArrayType(new MixedType(), new MixedType()); - case 'iterable': - return new IterableType(new MixedType(), new MixedType()); - case 'callable': - return new CallableType(); - case 'void': - return new VoidType(); - case 'object': - return new ObjectWithoutClassType(); - case 'mixed': - return new MixedType(true); - case 'self': - return $selfClass !== null ? new ObjectType($selfClass) : new ErrorType(); - case 'parent': - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { - $classReflection = $reflectionProvider->getClass($selfClass); - if ($classReflection->getParentClass() !== null) { - return new ObjectType($classReflection->getParentClass()->getName()); - } - } - return new NonexistentParentClassType(); - case 'static': - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { - return new StaticType($reflectionProvider->getClass($selfClass)); - } - - return new ErrorType(); - case 'null': - return new NullType(); - 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 ReflectionUnionType) { - $type = TypeCombinator::union(...array_map(static function (ReflectionType $type) use ($selfClass): Type { - return self::decideTypeFromReflection($type, null, $selfClass, false); - }, $reflectionType->getTypes())); + $type = TypeCombinator::union(...array_map(static fn (ReflectionType $type): Type => self::decideTypeFromReflection($type, selfClass: $selfClass), $reflectionType->getTypes())); return self::decideType($type, $phpDocType); } - if (!$reflectionType instanceof ReflectionNamedType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); - } + if ($reflectionType instanceof ReflectionIntersectionType) { + $types = []; + foreach ($reflectionType->getTypes() as $innerReflectionType) { + $innerType = self::decideTypeFromReflection($innerReflectionType, selfClass: $selfClass); + if (!$innerType->isObject()->yes()) { + return new NeverType(); + } - $reflectionTypeString = $reflectionType->getName(); - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\object')) { - $reflectionTypeString = 'object'; - } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\mixed')) { - $reflectionTypeString = 'mixed'; + $types[] = $innerType; + } + + return self::decideType(TypeCombinator::intersect(...$types), $phpDocType); } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\false')) { - $reflectionTypeString = 'false'; + + if (!$reflectionType instanceof ReflectionNamedType) { + throw new ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\null')) { - $reflectionTypeString = 'null'; + + if ($reflectionType->isIdentifier()) { + $typeNode = new Identifier($reflectionType->getName()); + } else { + $typeNode = new FullyQualified($reflectionType->getName()); } - $type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass); + $type = ParserNodeTypeToPHPStanType::resolve($typeNode, $selfClass); if ($reflectionType->allowsNull()) { $type = TypeCombinator::addNull($type); - } elseif ($phpDocType !== null) { - $phpDocType = TypeCombinator::removeNull($phpDocType); } return self::decideType($type, $phpDocType); @@ -114,20 +75,24 @@ public static function decideTypeFromReflection( public static function decideType( Type $type, - ?Type $phpDocType = null + ?Type $phpDocType, ): Type { + if ($phpDocType !== null && $type->isNull()->no()) { + $phpDocType = TypeCombinator::removeNull($phpDocType); + } + if ($type instanceof BenevolentUnionType) { + return $type; + } + if ($phpDocType !== null && !$phpDocType instanceof ErrorType) { if ($phpDocType instanceof NeverType && $phpDocType->isExplicit()) { return $phpDocType; } - if ($type instanceof VoidType) { - return new VoidType(); - } if ( $type instanceof MixedType && !$type->isExplicitMixed() - && $phpDocType instanceof VoidType + && $phpDocType->isVoid()->yes() ) { return $phpDocType; } @@ -136,27 +101,30 @@ public static function decideType( if ($phpDocType instanceof UnionType) { $innerTypes = []; foreach ($phpDocType->getTypes() as $innerType) { - if ($innerType instanceof ArrayType) { + if ($innerType instanceof ArrayType && $innerType->getKeyType()->describe(VerbosityLevel::typeOnly()) === 'mixed') { $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->getKeyType()->describe(VerbosityLevel::typeOnly()) === 'mixed') { $phpDocType = new IterableType( $phpDocType->getKeyType(), - $phpDocType->getItemType() + $phpDocType->getItemType(), ); } } if ( - (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) - && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) + || ( + (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) + && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ) ) { $resultType = $phpDocType; } else { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 08d347c9d9..3dee99c50d 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -2,42 +2,76 @@ 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\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\Reflection\MissingPropertyFromReflectionException; 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\TemplateIterableType; +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) { @@ -53,11 +87,10 @@ public function __construct(array $types) $throwException(); } - $this->types = UnionTypeHelper::sortTypes($types); } /** - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getTypes(): array { @@ -65,69 +98,180 @@ public function getTypes(): array } /** - * @return string[] + * @param callable(Type $type): bool $filterCb */ + public function filterTypes(callable $filterCb): Type + { + $newTypes = []; + $changed = false; + foreach ($this->getTypes() as $innerType) { + if (!$filterCb($innerType)) { + $changed = true; + continue; + } + + $newTypes[] = $innerType; + } + + if (!$changed) { + return $this; + } + + return TypeCombinator::union(...$newTypes); + } + + public function isNormalized(): bool + { + return $this->normalized; + } + + /** + * @return Type[] + */ + protected function getSortedTypes(): array + { + if ($this->sortedTypes) { + return $this->types; + } + + $this->types = UnionTypeHelper::sortTypes($this->types); + $this->sortedTypes = true; + + return $this->types; + } + public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->getTypes()); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - if ( - $type->equals(new ObjectType(\DateTimeInterface::class)) - && $this->accepts( - new UnionType([new ObjectType(\DateTime::class), new ObjectType(\DateTimeImmutable::class)]), - $strictTypes - )->yes() - ) { - return TrinaryLogic::createYes(); + return array_values(array_unique($this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassNames(), + static fn (Type $type) => $type->isObject()->yes(), + ))); + } + + public function getObjectClassReflections(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassReflections(), + static fn (Type $type) => $type->isObject()->yes(), + ); + } + + public function getArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantStrings(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantStrings(), + static fn (Type $type) => $type->isString()->yes(), + ); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if (!$type->equals(new ObjectType($baseClass))) { + continue; + } + + $union = TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + if ($this->accepts($union, $strictTypes)->yes()) { + return AcceptsResult::createYes(); + } + break; + } + + $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) { + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { return $type->isAcceptedBy($this, $strictTypes); } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->accepts($type, $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 isSuperTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { - if ($otherType instanceof self || $otherType instanceof IterableType) { + if ( + ($otherType instanceof self && !$otherType instanceof TemplateUnionType) + || ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType) + || $otherType instanceof NeverType + || $otherType instanceof ConditionalType + || $otherType instanceof ConditionalTypeForParameter + || $otherType instanceof IntegerRangeType + ) { return $otherType->isSubTypeOf($this); } $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOf($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); - return TrinaryLogic::createNo()->or(...$results); - } - - public function isSubTypeOf(Type $otherType): TrinaryLogic - { - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $otherType->isSuperTypeOf($innerType); + if ($otherType instanceof TemplateUnionType) { + return $result->or($otherType->isSubTypeOf($this)); } - return TrinaryLogic::extremeIdentity(...$results); + 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 @@ -140,25 +284,52 @@ public function equals(Type $type): bool return false; } - foreach ($this->types as $i => $innerType) { - if (!$innerType->equals($type->types[$i])) { + $otherTypes = $type->types; + foreach ($this->types as $innerType) { + $match = false; + foreach ($otherTypes as $i => $otherType) { + if (!$innerType->equals($otherType)) { + continue; + } + + $match = true; + unset($otherTypes[$i]); + break; + } + + if (!$match) { return false; } } - return true; + return count($otherTypes) === 0; } public function describe(VerbosityLevel $level): string { + if (isset($this->cachedDescriptions[$level->getLevelValue()])) { + return $this->cachedDescriptions[$level->getLevelValue()]; + } $joinTypes = static function (array $types) use ($level): string { $typeNames = []; - foreach ($types as $type) { + foreach ($types as $i => $type) { if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) { $typeNames[] = sprintf('(%s)', $type->describe($level)); + } elseif ($type instanceof TemplateType) { + $isLast = $i >= count($types) - 1; + $bound = $type->getBound(); + if ( + !$isLast + && ($level->isTypeOnly() || $level->isValue()) + && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType) + ) { + $typeNames[] = sprintf('(%s)', $type->describe($level)); + } else { + $typeNames[] = $type->describe($level); + } } elseif ($type instanceof IntersectionType) { $intersectionDescription = $type->describe($level); - if (strpos($intersectionDescription, '&') !== false) { + if (str_contains($intersectionDescription, '&')) { $typeNames[] = sprintf('(%s)', $type->describe($level)); } else { $typeNames[] = $intersectionDescription; @@ -168,70 +339,85 @@ public function describe(VerbosityLevel $level): string } } + if ($level->isPrecise() || $level->isCache()) { + $duplicates = array_diff_assoc($typeNames, array_unique($typeNames)); + if (count($duplicates) > 0) { + $indexByDuplicate = array_fill_keys($duplicates, 0); + foreach ($typeNames as $key => $typeName) { + if (!isset($indexByDuplicate[$typeName])) { + continue; + } + + $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName]; + } + } + } else { + $typeNames = array_unique($typeNames); + } + + if (count($typeNames) > 1024) { + return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}"; + } + return implode('|', $typeNames); }; - return $level->handle( + return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle( function () use ($joinTypes): string { $types = TypeCombinator::union(...array_map(static function (Type $type): Type { if ( - $type instanceof ConstantType - && !$type instanceof ConstantBooleanType + $type->isConstantValue()->yes() + && $type->isTrue()->or($type->isFalse())->no() ) { - return $type->generalize(GeneralizePrecision::moreSpecific()); + 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); @@ -248,27 +434,38 @@ 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->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -286,7 +483,73 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $propertiesCount = count($propertyPrototypes); if ($propertiesCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName)); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName)); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasStaticProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); } if ($propertiesCount === 1) { @@ -298,19 +561,15 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember public function canCallMethods(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -328,7 +587,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName); } if ($methodsCount === 1) { @@ -340,103 +599,170 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce public function canAccessConstants(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { return $this->hasInternal( - static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }, - static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - } + static fn (Type $type): TrinaryLogic => $type->canAccessConstants(), + static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), ); } - public function getConstant(string $constantName): ConstantReflection + 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->unionResults(static function (Type $type): TrinaryLogic { - return $type->isNumericString(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } public function isNonEmptyString(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isNonEmptyString(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + } + + public function isNonFalsyString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString()); } public function isLiteralString(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isLiteralString(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); + } + + public function isLowercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + + public function isClassString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString()); + } + + public function getClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType()); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType()); + } + + public function isVoid(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid()); + } + + public function isScalar(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar()); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return $this->notBenevolentUnionResults( + 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 @@ -460,153 +786,276 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->unionTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { - return $type->setOffsetValueType($offsetType, $valueType, $unionValues); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + } + + public function 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 getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict)); + } + + 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, ?TrinaryLogic $strict = null): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); + } + + 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 spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); + } + + 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): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThan($otherType); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion)); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThanOrEqual($otherType); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion)); } - public function getSmallerType(): Type + public function isNull(): TrinaryLogic { - return $this->unionTypes(static function (Type $type): Type { - return $type->getSmallerType(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull()); } - public function getSmallerOrEqualType(): Type + public function isConstantValue(): TrinaryLogic { - return $this->unionTypes(static function (Type $type): Type { - return $type->getSmallerOrEqualType(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); } - public function getGreaterType(): Type + public function isConstantScalarValue(): TrinaryLogic { - return $this->unionTypes(static function (Type $type): Type { - return $type->getGreaterType(); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); } - public function getGreaterOrEqualType(): Type + public function getConstantScalarTypes(): array { - return $this->unionTypes(static function (Type $type): Type { - return $type->getGreaterOrEqualType(); - }); + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes()); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function getConstantScalarValues(): array { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThan($type); - }); + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues()); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isTrue(): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThanOrEqual($type); - }); + 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(); @@ -631,10 +1080,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $myTypes = $this->types; } - $myTemplateTypes = []; foreach ($myTypes as $type) { if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) { - $myTemplateTypes[] = $type; continue; } $types = $types->union($type->inferTemplateTypes($receivedType)); @@ -695,31 +1142,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 */ - protected function unionResults(callable $getResult): TrinaryLogic + private function notBenevolentUnionResults(callable $getResult): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); + return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult); } /** * @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 0e33878645..f91ea5cb45 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -3,32 +3,21 @@ 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[] - */ - public static function getReferencedClasses(array $types): array - { - $subTypeClasses = []; - foreach ($types as $type) { - $subTypeClasses[] = $type->getReferencedClasses(); - } - - return array_merge(...$subTypeClasses); - } - - /** - * @param \PHPStan\Type\Type[] $types - * @return \PHPStan\Type\Type[] + * @param Type[] $types + * @return Type[] */ public static function sortTypes(array $types): array { @@ -45,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; @@ -94,27 +83,56 @@ public static function sortTypes(array $types): array return ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN); } + if ($a instanceof IntegerRangeType && $b instanceof IntegerType) { + return 1; + } + + if ($b instanceof IntegerRangeType && $a instanceof IntegerType) { + return -1; + } + if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) { - return strcasecmp($a->getValue(), $b->getValue()); + 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())); + } + + if ($a->isString()->yes() && $b->isString()->yes()) { + return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); } - return strcasecmp($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); + 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..6577289ff9 --- /dev/null +++ b/src/Type/UsefulTypeAliasResolver.php @@ -0,0 +1,157 @@ + */ + private array $resolvedGlobalTypeAliases = []; + + /** @var array */ + private array $resolvedLocalTypeAliases = []; + + /** @var array */ + private array $resolvingClassTypeAliases = []; + + /** @var array */ + private array $inProcess = []; + + /** + * @param array $globalTypeAliases + */ + public function __construct( + #[AutowiredParameter(ref: '%typeAliases%')] + 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 8ae7d0060d..59259d2df7 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -2,14 +2,19 @@ namespace PHPStan\Type; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; -class VerbosityLevel +final class VerbosityLevel { private const TYPE_ONLY = 1; @@ -20,19 +25,28 @@ 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 { @@ -57,24 +71,64 @@ public static function cache(): self return self::create(self::CACHE); } + public function isTypeOnly(): bool + { + return $this->value === self::TYPE_ONLY; + } + + public function isValue(): bool + { + return $this->value === self::VALUE; + } + + public function isPrecise(): bool + { + return $this->value === self::PRECISE; + } + + public function isCache(): bool + { + return $this->value === self::CACHE; + } + /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { - $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose): Type { + $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type { if ($type->isCallable()->yes()) { $moreVerbose = true; - return $type; + + // Keep checking if we need to be very verbose. + return $traverse($type); } - if ($type instanceof ConstantType && !$type instanceof NullType) { + if ($type->isConstantValue()->yes() && $type->isNull()->no()) { $moreVerbose = true; + + // For ConstantArrayType we need to keep checking if we need to be very verbose. + if (!$type->isArray()->no()) { + return $traverse($type); + } + return $type; } - if ($type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType) { + 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 NonEmptyArrayType) { + if ( + $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { $moreVerbose = true; + $veryVerbose = true; return $type; } if ($type instanceof IntegerRangeType) { @@ -86,19 +140,25 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc /** @var bool $moreVerbose */ $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptingType, $moreVerboseCallback); + if ($veryVerbose) { + return self::precise(); + } + if ($moreVerbose) { - return self::value(); + $verbosity = self::value(); } if ($acceptedType === null) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } $containsInvariantTemplateType = false; TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $reflection = $type->getClassReflection(); if ($reflection !== null) { $templateTypeMap = $reflection->getTemplateTypeMap(); @@ -121,14 +181,20 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc }); if (!$containsInvariantTemplateType) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } /** @var bool $moreVerbose */ $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return $moreVerbose ? self::value() : self::typeOnly(); + if ($veryVerbose) { + return self::precise(); + } + + return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); } /** @@ -136,13 +202,12 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc * @param callable(): string $valueCallback * @param callable(): string|null $preciseCallback * @param callable(): string|null $cacheCallback - * @return string */ public function handle( callable $typeOnlyCallback, callable $valueCallback, ?callable $preciseCallback = null, - ?callable $cacheCallback = null + ?callable $cacheCallback = null, ): string { if ($this->value === self::TYPE_ONLY) { @@ -161,19 +226,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 b2688f6ac5..83c5a05726 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -2,19 +2,26 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\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; @@ -22,40 +29,49 @@ class VoidType implements Type 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 { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean($type instanceof self); + return new AcceptsResult($type->isVoid()->or($type->isNull()), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -73,6 +89,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -93,7 +114,72 @@ 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(); } @@ -108,23 +194,79 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - * @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 @@ +> + * @return list',args?:list,object?:object}> * @throws void */ public function getTrace(); @@ -78,7 +78,7 @@ class Exception implements Throwable final public function getLine(): int {} /** - * @return list> + * @return list',args?:list,object?:object}> * @throws void */ final public function getTrace(): array {} @@ -125,7 +125,7 @@ class Error implements Throwable final public function getLine(): int {} /** - * @return list> + * @return list',args?:list,object?:object}> * @throws void */ final public function getTrace(): array {} diff --git a/stubs/ImagickPixel.stub b/stubs/ImagickPixel.stub new file mode 100644 index 0000000000..49ac0c9161 --- /dev/null +++ b/stubs/ImagickPixel.stub @@ -0,0 +1,9 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>} : ($normalized is 1 ? array{r: float, g: float, b: float, a: float} : ($normalized is 2 ? array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>} : array{}))) + */ + public function getColor(int $normalized = 0): array; +} diff --git a/stubs/PDOStatement.stub b/stubs/PDOStatement.stub index 79637d8370..f1b32e3d37 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -7,5 +7,21 @@ */ 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) {} + + /** + * @return Iterator + */ + public function getIterator() {} } diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index e5d2a0908a..06f8e08a2e 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -2,12 +2,12 @@ /** * @template-covariant T of object - * @property-read class-string $name */ class ReflectionClass { /** + * @readonly * @var class-string */ public $name; @@ -31,7 +31,7 @@ class ReflectionClass public function newInstance(...$args) {} /** - * @param array $args + * @param array $args * * @return T */ @@ -43,7 +43,7 @@ class ReflectionClass public function newInstanceWithoutConstructor(); /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionClassConstant.stub b/stubs/ReflectionClassConstant.stub index 669ccbef89..4396980e06 100644 --- a/stubs/ReflectionClassConstant.stub +++ b/stubs/ReflectionClassConstant.stub @@ -3,7 +3,7 @@ class ReflectionClassConstant { /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionClassWithLazyObjects.stub b/stubs/ReflectionClassWithLazyObjects.stub new file mode 100644 index 0000000000..1a53ae8750 --- /dev/null +++ b/stubs/ReflectionClassWithLazyObjects.stub @@ -0,0 +1,113 @@ + + */ + public $name; + + /** + * @param T|class-string $argument + * @throws ReflectionException + */ + public function __construct($argument) {} + + /** + * @return class-string + */ + public function getName() : string; + + /** + * @param mixed ...$args + * + * @return T + */ + public function newInstance(...$args) {} + + /** + * @param array $args + * + * @return T + */ + public function newInstanceArgs(array $args) {} + + /** + * @return T + */ + public function newInstanceWithoutConstructor(); + + /** + * @return list> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } + + /** + * @param callable(T): void $initializer + * @return T + */ + public function newLazyGhost(callable $initializer, int $options = 0): object + { + } + + /** + * @param callable(T): T $factory + * @return T + */ + public function newLazyProxy(callable $factory, int $options = 0): object + { + } + + /** + * @param T $object + * @param callable(T): void $initializer + */ + public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void + { + } + + /** + * @param T $object + * @param callable(T): T $factory + */ + public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void + { + } + + /** + * @param T $object + * @return T + */ + public function initializeLazyObject(object $object): object + { + } + + /** + * @param T $object + * @return T + */ + public function markLazyObjectAsInitialized(object $object): object + { + } + + /** + * @param T $object + */ + public function getLazyInitializer(object $object): ?callable + { + } + + /** + * @param T $object + */ + public function isUninitializedLazyObject(object $object): bool + { + } +} diff --git a/stubs/ReflectionEnum.stub b/stubs/ReflectionEnum.stub new file mode 100644 index 0000000000..9968c5c7e3 --- /dev/null +++ b/stubs/ReflectionEnum.stub @@ -0,0 +1,29 @@ + + */ +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 {} + + public function getBackingType(): ?ReflectionNamedType {} + +} diff --git a/stubs/ReflectionEnumWithLazyObjects.stub b/stubs/ReflectionEnumWithLazyObjects.stub new file mode 100644 index 0000000000..92ec6c660b --- /dev/null +++ b/stubs/ReflectionEnumWithLazyObjects.stub @@ -0,0 +1,29 @@ + + */ +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 {} + + public function getBackingType(): ?ReflectionNamedType {} + +} diff --git a/stubs/ReflectionFunctionAbstract.stub b/stubs/ReflectionFunctionAbstract.stub index 36e48df100..0154996741 100644 --- a/stubs/ReflectionFunctionAbstract.stub +++ b/stubs/ReflectionFunctionAbstract.stub @@ -9,7 +9,7 @@ abstract class ReflectionFunctionAbstract public function getFileName () {} /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionMethod.stub b/stubs/ReflectionMethod.stub new file mode 100644 index 0000000000..cb060a6c97 --- /dev/null +++ b/stubs/ReflectionMethod.stub @@ -0,0 +1,16 @@ +> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionProperty.stub b/stubs/ReflectionProperty.stub index 312688067d..002daeeeee 100644 --- a/stubs/ReflectionProperty.stub +++ b/stubs/ReflectionProperty.stub @@ -3,7 +3,7 @@ class ReflectionProperty { /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/SplObjectStorage.stub b/stubs/SplObjectStorage.stub index 72a75982dd..146785e522 100644 --- a/stubs/SplObjectStorage.stub +++ b/stubs/SplObjectStorage.stub @@ -5,9 +5,10 @@ * @template TData * * @template-implements Iterator + * @template-implements SeekableIterator * @template-implements ArrayAccess */ -class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess +class SplObjectStorage implements Countable, Iterator, SeekableIterator, Serializable, ArrayAccess { /** @@ -31,11 +32,6 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess */ public function detach(object $object): void { } - /** - * @param TObject $object - */ - public function detach(object $object): void { } - /** * @param TObject $object */ @@ -47,12 +43,12 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess public function getInfo() { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAll(SplObjectStorage $storage): void { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAllExcept(SplObjectStorage $storage): void { } diff --git a/stubs/WeakReference.stub b/stubs/WeakReference.stub index 5f23dbca9c..060dfe1a8d 100644 --- a/stubs/WeakReference.stub +++ b/stubs/WeakReference.stub @@ -26,4 +26,9 @@ final class WeakReference */ final class WeakMap implements \ArrayAccess, \Countable, \IteratorAggregate { + /** + * @param TKey $offset + * @return TValue + */ + public function offsetGet($offset) {} } diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 929aaa589b..3297e47316 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -17,51 +17,204 @@ function array_reduce( ) {} /** - * @template TKey of array-key - * @template TValue of mixed - * @template TUser of mixed + * @template T of mixed * - * @param array $one - * @param callable(TValue, TKey, TUser=): mixed $two - * @param TUser $three + * @param array $array + * @return ($array is non-empty-array ? non-empty-list : list) + */ +function array_values(array $array): array {} + +/** + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @return true + * @param TArray $array + * @param callable(T,T):int $callback */ -function array_walk( - array &$one, - callable $two, - $three = null -): bool {} +function uasort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback */ -function uasort( - array &$one, - callable $two -): bool {} +function usort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template TKey as (int|string) + * @template T + * @template TArray as array + * + * @param TArray $array + * @param callable(TKey,TKey):int $callback + */ +function uksort(array &$array, callable $callback): bool +{ +} + +/** + * @template TV of mixed + * @template TK of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_udiff( + array $one, + array $two, + callable $three +): array {} + +/** + * @param array $value + * @return ($value is list ? true : false) + */ +function array_is_list(array $value): bool {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_intersect_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * + * @return array + */ +function array_intersect_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * + * @return array + */ +function array_udiff_assoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_udiff_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect_assoc( + array $one, + array $two, + callable $three, +): array {} + +/** + * @template TK of array-key + * @template TV of mixed * - * @param array $one - * @param callable(T, T): int $two + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array */ -function usort( - array &$one, - callable $two -): bool {} +function array_uintersect_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} /** - * @template T of array-key + * @template TK of array-key + * @template TV of mixed * - * @param array $one - * @param callable(T, T): int $two + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array */ -function uksort( - array &$one, - callable $two -): bool {} +function array_uintersect( + array $one, + array $two, + callable $three, +): array {} diff --git a/stubs/core.stub b/stubs/core.stub new file mode 100644 index 0000000000..f608f51110 --- /dev/null +++ b/stubs/core.stub @@ -0,0 +1,379 @@ + $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 ? array|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 ? array|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 array|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 array|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) {} + +/** + * @param callable|int $handler + * @param-later-invoked-callable $handler + */ +function pcntl_signal(int $signal, $handler, bool $restart_syscalls = true): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function set_error_handler(?callable $callback, int $error_levels = E_ALL): ?callable {} + +/** + * @param-later-invoked-callable $callback + */ +function set_exception_handler(?callable $callback): ?callable {} + +/** + * @param-later-invoked-callable $callback + */ +function spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function register_shutdown_function(callable $callback, mixed ...$args): void {} + +/** + * @param-later-invoked-callable $callback + */ +function header_register_callback(callable $callback): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function register_tick_function(callable $callback, mixed ...$args): bool {} + +/** + * @template P of int + * + * @param string|list $command + * @param array|resource> $descriptor_spec + * @param mixed $pipes + * @param null|array $env_vars + * @param null|array $options + * + * @param-out array $pipes + * + * @return resource|false + */ +function proc_open($command, array $descriptor_spec, &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = 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..5f32a4763c 100644 --- a/stubs/dom.stub +++ b/stubs/dom.stub @@ -30,13 +30,24 @@ class DOMDocument class DOMNode { + /** + * @var DOMNamedNodeMap|null + */ + public $attributes; + + /** + * @phpstan-assert-if-true =DOMNamedNodeMap $this->attributes + * @return bool + */ + public function hasAttributes() {} + } class DOMElement extends DOMNode { - /** @var DOMDocument */ - public $ownerDocument; + /** @var DOMNamedNodeMap */ + public $attributes; /** * @param string $name @@ -55,9 +66,9 @@ class DOMElement extends DOMNode /** * @template-covariant TNode as DOMNode - * @implements Traversable + * @implements IteratorAggregate */ -class DOMNodeList implements Traversable +class DOMNodeList implements IteratorAggregate, Countable { /** @@ -68,60 +79,27 @@ class DOMNodeList implements Traversable } -class DOMAttr -{ - - /** @var DOMDocument */ - public $ownerDocument; - -} - -class DOMCharacterData -{ - - /** @var DOMDocument */ - public $ownerDocument; - -} - -class DOMCharacterData -{ - - /** @var DOMDocument */ - public $ownerDocument; - -} - -class DOMDocumentType -{ - - /** @var DOMDocument */ - public $ownerDocument; - -} - -class DOMEntity +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 DOMNotation +class DOMAttr extends DOMNode { - /** @var DOMDocument */ - public $ownerDocument; - } class DOMProcessingInstruction { - /** @var DOMDocument */ - public $ownerDocument; - /** * @var string */ @@ -135,11 +113,36 @@ class DOMProcessingInstruction } /** + * @template-covariant TNode as DOMNode + * @implements IteratorAggregate + * * @property-read int $length */ -class DOMNamedNodeMap +class DOMNamedNodeMap implements IteratorAggregate, Countable { + /** + * @return Iterator + */ + public function getIterator(): Iterator {} + /** + * @param string $qualifiedName + * @return TNode|null + */ + public function getNamedItem($qualifiedName): ?DOMNode {} + + /** + * @param string|null $namespace + * @param string $localName + * @return TNode|null + */ + public function getNamedItemNS($namespace, $localName): ?DOMNode {} + + /** + * @param int $index + * @return TNode|null + */ + public function item($index): ?DOMNode {} } class DOMText diff --git a/stubs/ext-ds.stub b/stubs/ext-ds.stub index df3a08d9e7..03acd69af5 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -2,22 +2,23 @@ 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(); @@ -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 { } @@ -80,6 +81,14 @@ final class Deque implements Sequence { } + /** + * @param (callable(TValue, TValue): int)|null $comparator + * @return Deque + */ + public function sorted(?callable $comparator = null): Deque + { + } + /** * @return Deque */ @@ -92,8 +101,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 +198,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 +292,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 +300,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 +308,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 +316,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 +356,8 @@ final class Map implements Collection } /** - * @template-covariant TKey - * @template-covariant TValue + * @template TKey + * @template TValue */ final class Pair implements JsonSerializable { @@ -380,8 +390,9 @@ final class Pair implements JsonSerializable /** * @template TValue * @extends Collection + * @extends ArrayAccess */ -interface Sequence extends Collection +interface Sequence extends Collection, ArrayAccess { /** * @param callable(TValue): TValue $callback @@ -389,11 +400,6 @@ interface Sequence extends Collection */ public function apply(callable $callback); - /** - * @return Sequence - */ - public function copy(); - /** * @param TValue ...$values */ @@ -403,7 +409,7 @@ interface Sequence extends Collection * @param (callable(TValue): bool)|null $callback * @return Sequence */ - public function filter(callable $callback = null); + public function filter(?callable $callback = null); /** * @param TValue $value @@ -434,7 +440,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 @@ -459,6 +465,7 @@ interface Sequence extends Collection /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function pop(); @@ -497,6 +504,7 @@ interface Sequence extends Collection /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function shift(); @@ -509,13 +517,13 @@ interface Sequence extends Collection * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null); + public function sort(?callable $comparator = null); /** * @param (callable(TValue, TValue): int)|null $comparator * @return Sequence */ - public function sorted(callable $comparator = null); + public function sorted(?callable $comparator = null); /** * @param TValue ...$values @@ -563,7 +571,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 { } @@ -571,7 +579,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 { } @@ -597,8 +605,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 @@ -641,7 +650,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 { } @@ -678,6 +687,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 @@ -687,6 +705,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 */ @@ -711,7 +739,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 { } @@ -719,7 +747,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 { } @@ -752,8 +780,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 @@ -780,6 +809,7 @@ final class Stack implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -804,8 +834,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 @@ -832,6 +863,7 @@ final class Queue implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -876,6 +908,7 @@ final class PriorityQueue implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { diff --git a/stubs/file.stub b/stubs/file.stub new file mode 100644 index 0000000000..48abcade87 --- /dev/null +++ b/stubs/file.stub @@ -0,0 +1,190 @@ +|false + */ +function scandir(string $directory, int $sorting_order = 0, $context = null) {} + +/** + * @phpstan-assert-if-true =non-empty-string $filename + */ +function is_writable(string $filename): bool {} + +/** + * @phpstan-assert-if-true =non-empty-string $filename + */ +function is_readable(string $filename): bool {} + +/** + * @phpstan-assert-if-true =non-empty-string $filename + */ +function is_executable(string $filename): bool {} + +/** + * @param ?resource $context + * @phpstan-assert-if-true =non-empty-string $filename + * @return list|false + */ +function file(string $filename, int $flags = 0, $context = null) {} + +/** + * @param ?resource $context + * @return string|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function file_get_contents(string $filename, bool $use_include_path = false, $context = null, int $offset = 0, ?int $length = null) {} + +/** + * @param ?resource $context + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function file_put_contents(string $filename, mixed $data, int $flags = 0, $context = null) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function fileatime(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function filectime(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function filegroup(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function fileinode(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function filemtime(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function fileowner(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function fileperms(string $filename) {} + +/** + * @return 0|positive-int|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function filesize(string $filename) {} + +/** + * @return string|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function filetype(string $filename) {} + +/** + * @param ?resource $context + * @return resource|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function fopen(string $filename, string $mode, bool $use_include_path = false, $context = null) {} + +/** + * @return int|false + * @phpstan-assert-if-true =non-empty-string $path + */ +function linkinfo(string $path) {} + +/** + * @return array|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function lstat(string $filename) {} + +/** + * @param ?resource $context + * @phpstan-assert-if-true =non-empty-string $directory + */ +function mkdir(string $directory, int $permissions = 0777, bool $recursive = false, $context = null): bool {} + +/** + * @return 0|positive-int|false + * @param ?resource $context + * @phpstan-assert-if-true =non-empty-string $filename + */ +function readfile(string $filename, bool $use_include_path = false, $context = null) {} + +/** + * @return non-empty-string|false + * @phpstan-assert-if-true =non-empty-string $path + */ +function readlink(string $path) {} + +/** + * @return non-empty-string|false + * @phpstan-assert-if-true =non-empty-string $path + */ +function realpath(string $path) {} + +/** + * @param ?resource $context + * @phpstan-assert-if-true =non-empty-string $directory + */ +function rmdir(string $directory, $context): bool {} + +/** + * @return array|false + * @phpstan-assert-if-true =non-empty-string $filename + */ +function stat(string $filename) {} + +/** + * @phpstan-assert-if-true =non-empty-string $filename + */ +function touch(string $filename, ?int $mtime, ?int $atime): bool {} + +/** + * @param ?resource $context + * @phpstan-assert-if-true =non-empty-string $filename + */ +function unlink(string $filename, $context = null): bool {} + 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 @@ + - * @implements ArrayAccess - * @implements Iterator - * @implements RecursiveIterator + * @implements Traversable + * @implements ArrayAccess + * @implements Iterator + * @implements RecursiveIterator */ 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) { } + + /** + * @param int|string|null $key + * @param self|string|int|float|bool $value + * @return void + */ + public function offsetSet($key, $value) {} + } /** @@ -147,15 +164,12 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Countable */ 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 ) { @@ -193,6 +207,52 @@ class IteratorIterator implements OuterIterator { public function __construct(Traversable $iterator) {} } +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @template-extends IteratorIterator + */ +class FilterIterator extends IteratorIterator +{ + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends FilterIterator + */ +class CallbackFilterIterator extends FilterIterator +{ + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends CallbackFilterIterator + * @implements RecursiveIterator + */ +class RecursiveCallbackFilterIterator extends CallbackFilterIterator implements RecursiveIterator +{ + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return RecursiveCallbackFilterIterator + */ + public function getChildren() {} + +} + /** * @template TKey of array-key * @template TValue @@ -228,3 +288,190 @@ class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator */ 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 deleted file mode 100644 index e41f42cfc4..0000000000 --- a/stubs/runtime/Attribute.php +++ /dev/null @@ -1,69 +0,0 @@ -flags = $flags; - } - - } -} - -if (\PHP_VERSION_ID < 80100 && !class_exists('ReturnTypeWillChange', false)) { - #[Attribute(Attribute::TARGET_METHOD)] - final class ReturnTypeWillChange - { - } -} diff --git a/stubs/runtime/Attribute84.php b/stubs/runtime/Attribute84.php new file mode 100644 index 0000000000..18d7de3da4 --- /dev/null +++ b/stubs/runtime/Attribute84.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/Attribute85.php b/stubs/runtime/Attribute85.php new file mode 100644 index 0000000000..37a522ea84 --- /dev/null +++ b/stubs/runtime/Attribute85.php @@ -0,0 +1,88 @@ +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 index 8f0dd0705b..19fe0a66ff 100644 --- a/stubs/runtime/ReflectionAttribute.php +++ b/stubs/runtime/ReflectionAttribute.php @@ -5,8 +5,11 @@ return; } - final class ReflectionAttribute + class ReflectionAttribute { + + public const IS_INSTANCEOF = 2; + public function getName(): string { } diff --git a/stubs/runtime/ReflectionIntersectionType.php b/stubs/runtime/ReflectionIntersectionType.php new file mode 100644 index 0000000000..764a9101a7 --- /dev/null +++ b/stubs/runtime/ReflectionIntersectionType.php @@ -0,0 +1,18 @@ +|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/stream_socket_client.stub b/stubs/stream_socket_client.stub new file mode 100644 index 0000000000..d5e6a7f13b --- /dev/null +++ b/stubs/stream_socket_client.stub @@ -0,0 +1,20 @@ +|\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/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 1ab24aa64d..cf8633673c 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -4,15 +4,19 @@ use Bug4288\MyClass; use Bug4713\Service; -use PHPStan\File\FileHelper; -use PHPStan\Reflection\ParametersAcceptorSelector; +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 PHPUnit\Framework\Attributes\RequiresPhp; +use function extension_loaded; +use function sprintf; use const PHP_VERSION_ID; -use function array_reverse; -class AnalyserIntegrationTest extends \PHPStan\Testing\PHPStanTestCase +class AnalyserIntegrationTest extends PHPStanTestCase { public function testUndefinedVariableFromAssignErrorHasLine(): void @@ -31,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 @@ -51,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 @@ -68,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); - $reflectionProvider = $this->createReflectionProvider(); - $this->assertTrue($reflectionProvider->hasClass(\ExtendingKnownClassWithCheck\Foo::class)); + $reflectionProvider = self::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 @@ -126,7 +125,7 @@ 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 @@ -150,17 +149,20 @@ public function testAnonymousClassWithWrongFilename(): void public function testExtendsPdoStatementCrash(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped(); - } $errors = $this->runAnalyse(__DIR__ . '/data/extends-pdo-statement.php'); - $this->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 @@ -176,45 +178,35 @@ public function testNestedNamespaces(): void public function testClassExistsAutoloadingError(): void { $errors = $this->runAnalyse(__DIR__ . '/data/class-exists.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testCollectWarnings(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Fatal error in PHP 8.0'); - } - restore_error_handler(); $errors = $this->runAnalyse(__DIR__ . '/data/declaration-warning.php'); - if (self::$useStaticReflectionProvider) { - $this->assertCount(1, $errors); - $this->assertSame('Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', $errors[0]->getMessage()); - $this->assertSame(22, $errors[0]->getLine()); - return; - } - $this->assertCount(2, $errors); - $messages = [ - 'Declaration of DeclarationWarning\Bar::doFoo(int $i): void should be compatible with DeclarationWarning\Foo::doFoo(): void', - 'Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', - ]; - if (PHP_VERSION_ID < 70400) { - $messages = array_reverse($messages); - } - foreach ($messages as $i => $message) { - $this->assertSame($message, $errors[$i]->getMessage()); - } + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', $errors[0]->getMessage()); + $this->assertSame(22, $errors[0]->getLine()); } public function testPropertyAssignIntersectionStaticTypeBug(): void { $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 testBug13424(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13424.php'); + $this->assertCount(1, $errors); + $this->assertSame('Instantiated class Bug13424\Hello not found.', $errors[0]->getMessage()); + $this->assertSame(14, $errors[0]->getLine()); } public function testTwoSameClassesInSingleFile(): void @@ -243,42 +235,54 @@ public function testTwoSameClassesInSingleFile(): void $this->assertSame(36, $error->getLine()); } + public function testBug6936(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6936.php'); + $this->assertNoErrors($errors); + } + public function testBug3405(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3405.php'); - $this->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); } public function testBug3468(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3468.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3686(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3686.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13352(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13352.php'); + $this->assertNoErrors($errors); } public function testBug3379(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-3379.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3379.php'); $this->assertCount(1, $errors); $this->assertSame('Constant SOME_UNKNOWN_CONST not found.', $errors[0]->getMessage()); } @@ -286,19 +290,19 @@ public function testBug3379(): void public function testBug3798(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3798.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3909(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3909.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4097(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4097.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4300(): void @@ -312,38 +316,65 @@ public function testBug4300(): void public function testBug4513(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4513.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug1871(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-1871.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3309(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3309.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); + } + + public function testBug11649(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11649.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6872(): void + { + $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->assertCount(0, $errors); + $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->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug1843(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-1843.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); + } + + public function 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 @@ -352,9 +383,9 @@ public function testBug4713(): void $this->assertCount(1, $errors); $this->assertSame('Method Bug4713\Service::createInstance() should return Bug4713\Service but returns object.', $errors[0]->getMessage()); - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass(Service::class); - $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('createInstance')->getVariants())->getParameters()[0]; + $parameter = $class->getNativeMethod('createInstance')->getOnlyVariant()->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); $this->assertInstanceOf(ConstantStringType::class, $defaultValue); $this->assertSame(Service::class, $defaultValue->getValue()); @@ -363,58 +394,60 @@ public function testBug4713(): void public function testBug4288(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4288.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass(MyClass::class); - $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('paginate')->getVariants())->getParameters()[0]; + $parameter = $class->getNativeMethod('paginate')->getOnlyVariant()->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); $this->assertInstanceOf(ConstantIntegerType::class, $defaultValue); $this->assertSame(10, $defaultValue->getValue()); $nativeProperty = $class->getNativeReflection()->getProperty('test'); - if (!method_exists($nativeProperty, 'getDefaultValue')) { - return; - } - - $this->assertSame(10, $nativeProperty->getDefaultValue()); + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $defaultValueType = $initializerExprTypeResolver->getType( + $nativeProperty->getDefaultValueExpression(), + InitializerExprContext::fromClassReflection($class->getNativeProperty('test')->getDeclaringClass()), + ); + $this->assertInstanceOf(ConstantIntegerType::class, $defaultValueType); + $this->assertSame(10, $defaultValueType->getValue()); } public function testBug4702(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4702.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testFunctionThatExistsOn72AndLater(): void { $errors = $this->runAnalyse(__DIR__ . '/data/ldap-exop-passwd.php'); - if (PHP_VERSION_ID >= 70200) { - $this->assertCount(0, $errors); + if (PHP_VERSION_ID < 80100) { + $this->assertNoErrors($errors); return; } $this->assertCount(1, $errors); - $this->assertSame('Function ldap_exop_passwd not found.', $errors[0]->getMessage()); + $this->assertSame('Parameter #1 $ldap of function ldap_exop_passwd expects LDAP\Connection, resource given.', $errors[0]->getMessage()); } public function testBug4715(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-4715.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.2')] public function testBug4734(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); - $this->assertCount(3, $errors); + $this->assertCount(5, $errors); // could be 3 - $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()); + $this->assertSame('Static property Bug4734\Foo::$httpMethodParameterOverride (bool) is never assigned false so the property type can be changed to true.', $errors[0]->getMessage()); // should not error + $this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so the property type can be changed to true.', $errors[1]->getMessage()); // should not error + $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[2]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[3]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[4]->getMessage()); } public function testBug5231(): void @@ -429,45 +462,1108 @@ public function testBug5231Two(): void $this->assertNotEmpty($errors); } + #[RequiresPhp('>= 8.1')] + public function testBug12512(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12512.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13218(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13218.php'); + $this->assertNoErrors($errors); + } + public function testBug5529(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-5529.php'); - $this->assertCount(0, $errors); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-5529.php'); + $this->assertNoErrors($errors); } public function testBug5527(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-5527.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug5639(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-5639.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug5657(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-5657.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } - /** - * @param string $file - * @return \PHPStan\Analyser\Error[] - */ - private function runAnalyse(string $file): array + #[RequiresPhp('>= 8.0')] + public function testBug5951(): void { - $file = $this->getFileHelper()->normalizePath($file); - /** @var \PHPStan\Analyser\Analyser $analyser */ + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5951.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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(1, $errors); + $this->assertSame('Call to an undefined method Bug6300\Bar::get().', $errors[0]->getMessage()); + $this->assertSame(27, $errors[0]->getLine()); + } + + public function testBug6466(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6466.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6494(): void + { + $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 testBug13057(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13057.php'); + $this->assertNoErrors($errors); + } + + 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()); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6114(): void + { + $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()); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6896(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testBug4308(): void + { + $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()); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7012(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7012.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6192(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testDiscussion6993(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7078(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7078.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7116(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7135(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7135.php'); + $this->assertCount(1, $errors); + $this->assertSame('Cannot create callable from the new operator.', $errors[0]->getMessage()); + } + + #[RequiresPhp('>= 8.0')] + public function testDiscussion7124(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-7124.php'); + $this->assertCount(4, $errors); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 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<\'bar\'|\'baz\'|\'foo\'|K of string, 5|6|7|bool|string> given.', $errors[5]->getMessage()); + $this->assertSame(29, $errors[5]->getLine()); + } + + #[RequiresPhp('>= 8.0')] + public function testOffsetAccess(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8078(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8078.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8072(): void + { + $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); + } + + #[RequiresPhp('>= 8.2')] + public function testAssertDocblock(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/assert-docblock.php'); + $this->assertCount(8, $errors); + $this->assertSame('Function AssertDocblock\validateStringArrayIfTrue() never returns false so the return type can be changed to true.', $errors[0]->getMessage()); + $this->assertSame(17, $errors[0]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringArrayIfFalse() never returns true so the return type can be changed to false.', $errors[1]->getMessage()); + $this->assertSame(25, $errors[1]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringOrIntArray() never returns true so the return type can be changed to false.', $errors[2]->getMessage()); + $this->assertSame(34, $errors[2]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringOrNonEmptyIntArray() never returns true so the return type can be changed to false.', $errors[3]->getMessage()); + $this->assertSame(44, $errors[3]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[4]->getMessage()); + $this->assertSame(218, $errors[4]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with string will always evaluate to true.', $errors[5]->getMessage()); + $this->assertSame(224, $errors[5]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with int will always evaluate to true.', $errors[6]->getMessage()); + $this->assertSame(232, $errors[6]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with int will always evaluate to false.', $errors[7]->getMessage()); + $this->assertSame(238, $errors[7]->getLine()); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8147(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8147.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12934(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8537(): void + { + $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()); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8983(): void + { + $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 testBug13507(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13507.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()); + } + + #[RequiresPhp('>= 8.0')] + public function testDiscussion9053(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php'); + $this->assertNoErrors($errors); + } + + public function testBug13492(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13492.php'); + $this->assertNoErrors($errors); + } + + public function testProcessCalledMethodInfiniteLoop(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/process-called-method-infinite-loop.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9428(): void + { + $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()); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9994(): void + { + $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()); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10049(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10086(): void + { + $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); + } + + #[RequiresPhp('>= 8.2')] + public function testBug10302(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10847(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10847.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10772(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10772.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10985(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10985.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10979(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11263(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11263.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11147(): void + { + $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()); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11283(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11297(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug5597(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11511(): void + { + $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 testBug12214(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12214.php'); + $this->assertNoErrors($errors); + } + + public function testBug11640(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11640.php'); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11709(): void + { + $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); + } + + #[RequiresPhp('>= 8.3')] + public function testBug12549(): void + { + $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); + } + + #[RequiresPhp('>= 8.3')] + public function testBug12159(): void + { + $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); + } + + #[RequiresPhp('>= 8.3')] + public function testBug12949(): void + { + $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()); + } + + public function testBug12979(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12979.php'); + $this->assertNoErrors($errors); + } + + public function testBug12095(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12095.php'); + $this->assertNoErrors($errors); + } + + public function testBug13279(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13279.php'); + $this->assertCount(1, $errors); + $this->assertSame('Parameter #2 $offset of function array_splice expects int, string given.', $errors[0]->getMessage()); + } + + public function testBug13310(): void + { + // require file to make sure the defined function is known + require_once __DIR__ . '/data/bug-13310.php'; + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13310.php'); + $this->assertNoErrors($errors); + } + + /** + * @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 0de0a0f1a3..8714dc736f 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,21 +2,49 @@ namespace PHPStan\Analyser; -use PhpParser\NodeVisitor\NodeConnectingVisitor; -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\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Nette\NetteContainer; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; -use PHPStan\NodeVisitor\StatementOrderVisitor; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Parser\RichParser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\AlwaysFailRule; -use PHPStan\Rules\Registry; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; - -class AnalyserTest extends \PHPStan\Testing\PHPStanTestCase +use PHPUnit\Framework\Attributes\DataProvider; +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 @@ -39,28 +67,98 @@ 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 testIgnoringBrokenConfigurationDoesNotWork(): void + public function testFileWithAnIgnoredErrorRawMessage(): void { - $this->markTestIncomplete(); - $result = $this->runAnalyser(['#was not found while trying to analyse it#'], true, __DIR__ . '/../../notAutoloaded/Baz.php', false); + $result = $this->runAnalyser([['rawMessage' => '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('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 excludePaths instead.', $result[1]); + $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 testFileWithAnIgnoredErrorRawMessageAndWrongIdentifier(): void + { + $result = $this->runAnalyser([['rawMessage' => '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 testFileWithAnIgnoredErrorRawMessageAndCorrectIdentifier(): void + { + $result = $this->runAnalyser([['rawMessage' => '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 testFileWithAnIgnoredErrorIdentifiers(): void + { + $result = $this->runAnalyser([['identifiers' => ['tests.alwaysFail']]], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertNoErrors($result); + } + + public function testFileWithAnIgnoredErrorIdentifiersWithPath(): void + { + $result = $this->runAnalyser([['identifiers' => ['tests.alwaysFail'], 'path' => __DIR__ . '/data/bootstrap-error.php']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertNoErrors($result); + } + + public function testFileWithAnIgnoredErrorIdentifiersWithWrongIdentifier(): void + { + $result = $this->runAnalyser([['identifiers' => ['wrong.identifier']]], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + $this->assertInstanceOf(Error::class, $result[0]); + $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 testIgnoreErrorByPath(): void @@ -72,23 +170,102 @@ 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 static 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', + ], + ], + ]; + + yield [ + [ + [ + 'rawMessage' => 'Fail.', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'rawMessage' => 'Fail.', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'rawMessage' => 'Fail.', + 'count' => 1, + 'path' => __DIR__ . '/data/two-fails.php', + ], ], ]; + } + + /** + * @param mixed[] $ignoreErrors + */ + #[DataProvider('dataIgnoreErrorByPathAndCount')] + 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 + public static function dataTrueAndFalse(): array { return [ [true], @@ -96,10 +273,31 @@ public function dataTrueAndFalse(): array ]; } - /** - * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles - */ + #[DataProvider('dataTrueAndFalse')] + public function testIgnoreErrorByPathAndIdentifierCountsCorrectly(bool $onlyFiles): void + { + $ignoreErrors = [ + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 2, + 'path' => __DIR__ . '/data/two-different-fails.php', + ], + ]; + + $filesToAnalyze = [ + __DIR__ . '/data/two-fails.php', + __DIR__ . '/data/two-different-fails.php', + ]; + $result = $this->runAnalyser($ignoreErrors, true, $filesToAnalyze, $onlyFiles); + $this->assertNoErrors($result); + } + + #[DataProvider('dataTrueAndFalse')] public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): void { $ignoreErrors = [ @@ -128,10 +326,7 @@ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): $this->assertSamePaths(__DIR__ . '/data/two-fails.php', $result[2]->getFile()); } - /** - * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreErrorByPathAndCountLessThanExpected(bool $onlyFiles): void { $ignoreErrors = [ @@ -191,7 +386,34 @@ public function testIgnoreErrorByPaths(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); - $this->assertCount(0, $result); + $this->assertNoErrors($result); + } + + public function testIgnoreErrorRawByPaths(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => 'Fail.', + 'paths' => [__DIR__ . '/data/bootstrap-error.php'], + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); + $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 @@ -224,6 +446,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 = [ @@ -237,7 +475,21 @@ 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 dataIgnoreErrorInTraitUsingClassFilePath(): array + 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 static function dataIgnoreErrorInTraitUsingClassFilePath(): array { return [ [ @@ -249,10 +501,7 @@ public function dataIgnoreErrorInTraitUsingClassFilePath(): array ]; } - /** - * @dataProvider dataIgnoreErrorInTraitUsingClassFilePath - * @param string $pathToIgnore - */ + #[DataProvider('dataIgnoreErrorInTraitUsingClassFilePath')] public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): void { $ignoreErrors = [ @@ -265,7 +514,22 @@ 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); + } + + #[DataProvider('dataIgnoreErrorInTraitUsingClassFilePath')] + public function testIgnoreErrorInTraitUsingClassFilePathWithPartialAnalysis(string $pathToIgnore): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail\.#', + 'path' => $pathToIgnore, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, [ + __DIR__ . '/data/traits-ignore/Foo.php', + ], true); + $this->assertNoErrors($result); } public function testIgnoredErrorMissingMessage(): void @@ -284,32 +548,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 @@ -328,10 +567,7 @@ public function testReportMultipleParserErrorsAtOnce(): void $this->assertSame(10, $errorTwo->getLine()); } - /** - * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles - */ + #[DataProvider('dataTrueAndFalse')] public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalysed(bool $onlyFiles): void { $ignoreErrors = [ @@ -347,13 +583,10 @@ 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 - */ + #[DataProvider('dataTrueAndFalse')] public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasNotAnalysed(bool $onlyFiles): void { $ignoreErrors = [ @@ -371,40 +604,38 @@ 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 - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void { $result = $this->runAnalyser([], $reportUnmatchedIgnoredErrors, [ @@ -428,105 +659,206 @@ 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 testIgnoreErrorExplicitReportUnmatchedDisableRaw(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => '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 testIgnoreErrorExplicitReportUnmatchedEnableRaw(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => '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([]))), + self::getContainer()->getByType(RuleErrorTransformer::class), + $this->createScopeFactory( + self::createReflectionProvider(), + self::getContainer()->getService('typeSpecifier'), + ), + new LocalIgnoresProcessor(), + $reportUnmatchedIgnoredErrors, + ); + $analyserResult = $finalizer->finalize($analyserResult, $onlyFiles, false)->getAnalyserResult(); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); + $errors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); + $errors = array_merge($errors, $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages()); if ($analyserResult->hasReachedInternalErrorsCountLimit()) { $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', 50); } return array_merge( $errors, - $analyserResult->getInternalErrors() + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()), ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): \PHPStan\Analyser\Analyser + private function createAnalyser(): Analyser { - $registry = new Registry([ + $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), ]); + $collectorRegistry = new CollectorRegistry([]); - $reflectionProvider = $this->createReflectionProvider(); - $printer = new \PhpParser\PrettyPrinter\Standard(); + $reflectionProvider = self::createReflectionProvider(); $fileHelper = $this->getFileHelper(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class)); $nodeScopeResolver = new NodeScopeResolver( $reflectionProvider, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), + 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, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), + self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), false, true, + true, [], [], - true + [stdClass::class], + true, + $this->shouldTreatPhpDocTypesAsCertain(), + true, ); - $lexer = new \PhpParser\Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); + $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, new RichParser( - new \PhpParser\Parser\Php7($lexer), - new \PhpParser\NodeVisitor\NameResolver(), - new NodeConnectingVisitor(), - new StatementOrderVisitor() + new Php7($lexer), + new NameResolver(), + self::getContainer(), + new IgnoreLexer(), ), - new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, $printer)), - $reportUnmatchedIgnoredErrors + new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($reflectionProvider, $fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), + self::getContainer()->getByType(RuleErrorTransformer::class), + 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 7c908f6742..914064439f 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -2,14 +2,22 @@ namespace PHPStan\Analyser; +use Override; use PHPStan\File\FileHelper; - -class AnalyserTraitsIntegrationTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_map; +use function array_merge; +use function array_unique; +use function sprintf; +use function usort; + +class AnalyserTraitsIntegrationTest extends PHPStanTestCase { - /** @var \PHPStan\File\FileHelper */ - private $fileHelper; + private FileHelper $fileHelper; + #[Override] protected function setUp(): void { $this->fileHelper = self::getContainer()->getByType(FileHelper::class); @@ -35,7 +43,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 +60,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 +68,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 +108,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 +118,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 +128,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 +153,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 +168,60 @@ public function testMissingReturnInAbstractTraitMethod(): void __DIR__ . '/traits/TraitWithAbstractMethod.php', __DIR__ . '/traits/ClassImplementingTraitWithAbstractMethod.php', ]); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); + } + + #[RequiresPhp('>= 8.1')] + public function testUnititializedReadonlyPropertyAccessedInTrait(): void + { + $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/AnalyserWithCheckDynamicPropertiesTest.php b/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php new file mode 100644 index 0000000000..8e209d7b93 --- /dev/null +++ b/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php @@ -0,0 +1,53 @@ +runAnalyse(__DIR__ . '/data/bug-13529.php'); + $this->assertCount(1, $errors); + $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); + $this->assertSame(8, $errors[0]->getLine()); + } + + /** + * @return Error[] + */ + private function runAnalyse(string $file): array + { + $file = $this->getFileHelper()->normalizePath($file); + + $analyser = self::getContainer()->getByType(Analyser::class); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true), + false, + true, + )->getErrors(); + foreach ($errors as $error) { + $this->assertSame($file, $error->getFilePath()); + } + + return $errors; + } + + public static function getAdditionalConfigFiles(): array + { + return array_unique( + array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/checkDynamicProperties.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 c5f5a5aab1..ab344177ba 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php @@ -10,7 +10,7 @@ class AnonymousClassNameRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AnonymousClassNameRule($reflectionProvider); } diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php new file mode 100644 index 0000000000..9b92249486 --- /dev/null +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php @@ -0,0 +1,164 @@ += 8.0')] + public function testArgumentReorderAllNamed(): 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 LNumber(0), + name: new Identifier('flags'), + ), + new Arg( + new String_('my json value'), + name: 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 + */ + #[RequiresPhp('>= 8.0')] + public function testArgumentReorderAllNamedWithSkipped(): 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 LNumber(128), + name: new Identifier('depth'), + ), + new Arg( + new String_('my json value'), + name: 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); + } + + #[RequiresPhp('>= 8.0')] + public function testMissingRequiredParameter(): 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 LNumber(128), + name: 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..b2cc10be5d --- /dev/null +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php @@ -0,0 +1,368 @@ + $parameterSettings + * @param array $argumentSettings + * @param array $expectedArgumentTypes + */ + #[DataProvider('dataReorderValid')] + 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), name: $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 static 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'], + ], + ]; + } + + /** + * @param array $parameterSettings + * @param array $argumentSettings + */ + #[DataProvider('dataReorderInvalid')] + 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), name: $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..579e52e359 --- /dev/null +++ b/tests/PHPStan/Analyser/AssertStubTest.php @@ -0,0 +1,37 @@ +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..6fa62ea5e7 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10922Test.php @@ -0,0 +1,37 @@ +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..51422dcd45 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10980Test.php @@ -0,0 +1,29 @@ +assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/Bug11009Test.php b/tests/PHPStan/Analyser/Bug11009Test.php new file mode 100644 index 0000000000..d558eb0a0f --- /dev/null +++ b/tests/PHPStan/Analyser/Bug11009Test.php @@ -0,0 +1,37 @@ +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..a7342e7506 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -0,0 +1,41 @@ + + */ +class Bug9307CallMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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..1de98619ea --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307Test.php @@ -0,0 +1,37 @@ +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 index 02081f8433..2d1cb36f58 100644 --- a/tests/PHPStan/Analyser/ClassConstantStubFileTest.php +++ b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php @@ -3,25 +3,24 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ClassConstantStubFileTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-stub-files.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/class-constant-stub-files.php'); } /** - * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php new file mode 100644 index 0000000000..af19568166 --- /dev/null +++ b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php @@ -0,0 +1,37 @@ +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..8b94d130e0 --- /dev/null +++ b/tests/PHPStan/Analyser/DoNotPolluteScopeWithBlockTest.php @@ -0,0 +1,37 @@ +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..75a9797ee3 --- /dev/null +++ b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/do-not-remember-possibly-impure-function-values.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php index bdb8094fe9..9a55ab9360 100644 --- a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -3,25 +3,30 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use const PHP_VERSION_ID; class DynamicMethodThrowTypeExtensionTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); + if (PHP_VERSION_ID < 80000) { + return []; + } + + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args-fixture.php'); } /** - * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php index c1d5a736d3..d336534264 100644 --- a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -3,26 +3,33 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use const PHP_VERSION_ID; class DynamicReturnTypeExtensionTypeInferenceTest extends TypeInferenceTestCase { - public function dataAsserts(): iterable + public static function dataAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types-named-args.php'); + } + + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-7344.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-7391b.php'); } /** - * @dataProvider dataAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataAsserts')] public function testAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index ec7647c2f3..e59abafff3 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -2,7 +2,10 @@ namespace PHPStan\Analyser; -class ErrorTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class ErrorTest extends PHPStanTestCase { public function testError(): void @@ -13,4 +16,41 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } + public static 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 static 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/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php new file mode 100644 index 0000000000..56b699f1fc --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -0,0 +1,218 @@ + yield (exit());', + false, + ], + [ + '(fn() => exit())();', // immediately invoked function expression + true, + ], + [ + 'register_shutdown_function(fn() => exit());', + false, + ], + [ + '@exit();', + true, + ], + [ + '$x && exit();', + false, + ], + [ + 'exit() && $x;', + true, + ], + [ + 'exit() || $x;', + true, + ], + [ + 'exit() ?? $x;', + true, + ], + [ + 'call_user_func(fn() => exit());', + true, + ], + [ + '(function() { exit(); })();', + true, + ], + [ + 'function () {};', + false, + ], + [ + 'call_user_func(function() { exit(); });', + true, + ], + [ + 'usort($arr, static function($a, $b):int { return $a <=> $b; });', + false, + ], + [ + 'var_dump(1+exit());', + true, + ], + [ + 'var_dump(1-exit());', + true, + ], + [ + 'var_dump(1*exit());', + true, + ], + [ + 'var_dump(1**exit());', + true, + ], + [ + 'var_dump(1/exit());', + true, + ], + [ + 'var_dump("a".exit());', + true, + ], + [ + 'var_dump(exit()."a");', + true, + ], + [ + 'array_push($arr, fn() => "exit");', + false, + ], + [ + 'array_push($arr, function() { exit(); });', + false, + ], + [ + 'array_push($arr, "exit");', + false, + ], + [ + 'array_unshift($arr, "exit");', + false, + ], + ]; + } + + #[DataProvider('dataIsAlwaysTerminating')] + public function testIsAlwaysTerminating( + string $code, + bool $expectedIsAlwaysTerminating, + ): void + { + /** @var Parser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + + /** @var Stmt[] $stmts */ + $stmts = $parser->parseString(sprintf('expr; + + /** @var NodeScopeResolver $nodeScopeResolver */ + $nodeScopeResolver = self::getContainer()->getByType(NodeScopeResolver::class); + /** @var ScopeFactory $scopeFactory */ + $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); + $scope = $scopeFactory->create(ScopeContext::create('test.php')) + ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); + + $result = $nodeScopeResolver->processExprNode( + $stmt, + $expr, + $scope, + static function (): void { + }, + ExpressionContext::createTopLevel(), + ); + $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); + } + +} diff --git a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php new file mode 100644 index 0000000000..6c3d707942 --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -0,0 +1,36 @@ +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..6e46630df6 --- /dev/null +++ b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php @@ -0,0 +1,97 @@ + $expectedTokens + */ + #[DataProvider('dataTokenize')] + 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..8a9e2ee7a6 --- /dev/null +++ b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php @@ -0,0 +1,38 @@ +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..ea3288772f --- /dev/null +++ b/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionTest.php @@ -0,0 +1,40 @@ + + */ +class InstanceMethodsParameterScopeFunctionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InstanceMethodsParameterScopeFunctionRule(); + } + + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/instance-methods-parameter-scope.php'], [ + [ + 'Name DateTime found in function scope null', + 12, + ], + [ + 'Name Baz\Waldo found in function scope null', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 7e1f8e5af3..22f21b50a2 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -3,8 +3,11 @@ namespace PHPStan\Analyser; use Generator; +use PhpParser\Node; use PhpParser\Node\Expr\Exit_; +use PHPStan\Node\Printer\Printer; use PHPStan\Node\VirtualNode; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\TypeInferenceTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; @@ -13,18 +16,29 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use SomeNodeScopeResolverNamespace\Foo; +use function define; +use function defined; +use function function_exists; +use function is_bool; +use function is_float; +use function is_int; +use function is_string; +use function sprintf; +use function str_replace; use const PHP_VERSION_ID; class LegacyNodeScopeResolverTest extends TypeInferenceTestCase { /** @var Scope[][] */ - private static $assertTypesCache = []; + private static array $assertTypesCache = []; public function testClassMethodScope(): void { - $this->processFile(__DIR__ . '/data/class.php', function (\PhpParser\Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -51,11 +65,10 @@ public function testClassMethodScope(): void }); } - private function getFileScope(string $filename): Scope + private static 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 { + self::processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { if (!($node instanceof Exit_)) { return; } @@ -63,10 +76,11 @@ private function getFileScope(string $filename): Scope $testScope = $scope; }); + /** @var Scope */ return $testScope; } - public function dataUnionInCatch(): array + public static function dataUnionInCatch(): array { return [ [ @@ -76,24 +90,20 @@ public function dataUnionInCatch(): array ]; } - /** - * @dataProvider dataUnionInCatch - * @param string $description - * @param string $expression - */ + #[DataProvider('dataUnionInCatch')] public function testUnionInCatch( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/catch-union.php', $description, - $expression + $expression, ); } - public function dataUnionAndIntersection(): array + public static function dataUnionAndIntersection(): array { return [ [ @@ -165,7 +175,7 @@ public function dataUnionAndIntersection(): array 'self::IPSUM_CONSTANT', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', 'parent::PARENT_CONSTANT', ], [ @@ -203,26 +213,22 @@ public function dataUnionAndIntersection(): array ]; } - /** - * @dataProvider dataUnionAndIntersection - * @param string $description - * @param string $expression - */ + #[DataProvider('dataUnionAndIntersection')] public function testUnionAndIntersection( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-intersection.php', $description, - $expression + $expression, ); } - public function dataAssignInIf(): array + public static function dataAssignInIf(): array { - $testScope = $this->getFileScope(__DIR__ . '/data/if.php'); + $testScope = self::getFileScope(__DIR__ . '/data/if.php'); return [ [ @@ -252,19 +258,19 @@ public function dataAssignInIf(): array $testScope, 'arrOne', TrinaryLogic::createYes(), - 'array(\'one\')', + 'array{\'one\'}', ], [ $testScope, 'arrTwo', TrinaryLogic::createYes(), - 'array(\'test\' => \'two\', 0 => Foo)', + 'array{test: \'two\', 0: Foo}', ], [ $testScope, 'arrThree', TrinaryLogic::createYes(), - 'array(\'three\')', + 'array{\'three\'}', ], [ $testScope, @@ -276,31 +282,31 @@ public function dataAssignInIf(): array $testScope, 'i', TrinaryLogic::createYes(), - 'int', + 'int<0, 4>', ], [ $testScope, 'f', TrinaryLogic::createMaybe(), - 'int', + 'int<1, max>', ], [ $testScope, 'anotherF', TrinaryLogic::createYes(), - 'int', + 'int<1, max>', ], [ $testScope, 'matches', TrinaryLogic::createYes(), - 'mixed', + 'array{0?: string}', ], [ $testScope, 'anotherArray', TrinaryLogic::createYes(), - 'array(\'test\' => array(\'another\'))', + 'array{test: array{\'another\'}}', ], [ $testScope, @@ -335,7 +341,7 @@ public function dataAssignInIf(): array $testScope, 'matches2', TrinaryLogic::createMaybe(), - 'mixed', + 'array{0?: string}', ], [ $testScope, @@ -347,13 +353,13 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'mixed', + 'array{}|array{string}', ], [ $testScope, 'matches4', TrinaryLogic::createMaybe(), - 'mixed', + 'array{}|array{string}', ], [ $testScope, @@ -407,13 +413,13 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'mixed', + 'array{string}', ], [ $testScope, 'previousI', TrinaryLogic::createYes(), - '0|1', + 'int<1, max>', ], [ $testScope, @@ -485,13 +491,13 @@ public function dataAssignInIf(): array $testScope, 'nullableIntegers', TrinaryLogic::createYes(), - 'array(1, 2, 3, null)', + 'array{1, 2, 3, null}', ], [ $testScope, 'union', TrinaryLogic::createYes(), - 'array(1, 2, 3, \'foo\')', + 'array{1, 2, 3, \'foo\'}', '1|2|3|\'foo\'', ], [ @@ -574,14 +580,14 @@ public function dataAssignInIf(): array [ $testScope, 'nonexistentVariableOutsideFor', - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), '1', ], [ $testScope, 'integerOrNullFromFor', TrinaryLogic::createYes(), - '1|null', + '1', ], [ $testScope, @@ -659,7 +665,7 @@ public function dataAssignInIf(): array $testScope, 'arrayOfIntegers', TrinaryLogic::createYes(), - 'array(1, 2, 3)', + 'array{1, 2, 3}', ], [ $testScope, @@ -689,7 +695,7 @@ public function dataAssignInIf(): array $testScope, 'mixed', TrinaryLogic::createYes(), - 'mixed', // should be mixed~bool+1 + 'mixed~bool', ], [ $testScope, @@ -730,20 +736,13 @@ public function dataAssignInIf(): array ]; } - /** - * @dataProvider dataAssignInIf - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param \PHPStan\TrinaryLogic $expectedCertainty - * @param string|null $typeDescription - * @param string|null $iterableValueTypeDescription - */ + #[DataProvider('dataAssignInIf')] public function testAssignInIf( Scope $scope, string $variableName, TrinaryLogic $expectedCertainty, ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null + ?string $iterableValueTypeDescription = null, ): void { $this->assertVariables( @@ -751,13 +750,13 @@ public function testAssignInIf( $variableName, $expectedCertainty, $typeDescription, - $iterableValueTypeDescription + $iterableValueTypeDescription, ); } - public function dataConstantTypes(): array + public static function dataConstantTypes(): array { - $testScope = $this->getFileScope(__DIR__ . '/data/constantTypes.php'); + $testScope = self::getFileScope(__DIR__ . '/data/constantTypes.php'); return [ [ @@ -783,7 +782,7 @@ public function dataConstantTypes(): array [ $testScope, 'literalArray', - 'array(\'a\' => 2, \'b\' => 4, \'c\' => 2, \'d\' => 4)', + 'array{a: 2, b: 4, c: 2, d: 4}', ], [ $testScope, @@ -813,17 +812,17 @@ public function dataConstantTypes(): array [ $testScope, 'incrementInForLoop', - 'int', + 'int<2, max>', ], [ $testScope, 'valueOverwrittenInForLoop', - '1|2', + '2', ], [ $testScope, 'arrayOverwrittenInForLoop', - 'array(\'a\' => int, \'b\' => \'bar\'|\'foo\')', + 'array{a: int<2, max>, b: \'bar\'}', ], [ $testScope, @@ -833,12 +832,12 @@ public function dataConstantTypes(): array [ $testScope, 'intProperty', - 'int', + 'int<2, max>', ], [ $testScope, 'staticIntProperty', - 'int', + 'int<2, max>', ], [ $testScope, @@ -853,7 +852,7 @@ public function dataConstantTypes(): array [ $testScope, 'variableIncrementedInClosurePassedByReference', - 'int', + 'int<0, max>', ], [ $testScope, @@ -863,7 +862,7 @@ public function dataConstantTypes(): array [ $testScope, 'yetAnotherVariableInClosurePassedByReference', - 'int', + '0|1', ], [ $testScope, @@ -873,16 +872,11 @@ public function dataConstantTypes(): array ]; } - /** - * @dataProvider dataConstantTypes - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param string $typeDescription - */ + #[DataProvider('dataConstantTypes')] public function testConstantTypes( Scope $scope, string $variableName, - string $typeDescription + string $typeDescription, ): void { $this->assertVariables( @@ -890,7 +884,7 @@ public function testConstantTypes( $variableName, TrinaryLogic::createYes(), $typeDescription, - null + null, ); } @@ -899,18 +893,18 @@ private function assertVariables( string $variableName, TrinaryLogic $expectedCertainty, ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null + ?string $iterableValueTypeDescription = null, ): void { $certainty = $scope->hasVariableType($variableName); $this->assertTrue( $expectedCertainty->equals($certainty), sprintf( - 'Certainty of variable $%s is %s, expected %s', + 'Certainty of %s is %s, expected %s', $variableName, $certainty->describe(), - $expectedCertainty->describe() - ) + $expectedCertainty->describe(), + ), ); if (!$expectedCertainty->no()) { if ($typeDescription === null) { @@ -920,14 +914,14 @@ private function assertVariables( $this->assertSame( $typeDescription, $scope->getVariableType($variableName)->describe(VerbosityLevel::precise()), - sprintf('Type of variable $%s does not match the expected one.', $variableName) + sprintf('Type of variable $%s does not match the expected one.', $variableName), ); if ($iterableValueTypeDescription !== null) { $this->assertSame( $iterableValueTypeDescription, $scope->getVariableType($variableName)->getIterableValueType()->describe(VerbosityLevel::precise()), - sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName) + sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName), ); } } elseif ($typeDescription !== null) { @@ -935,13 +929,13 @@ private function assertVariables( sprintf( 'No type should be asserted for an undefined variable $%s, %s given.', $variableName, - $typeDescription - ) + $typeDescription, + ), ); } } - public function dataArrayDestructuring(): array + public static function dataArrayDestructuring(): array { return [ [ @@ -1073,7 +1067,7 @@ public function dataArrayDestructuring(): array '$secondStringArray', ], [ - 'string', + 'non-empty-string', '$thirdStringArray', ], [ @@ -1089,7 +1083,7 @@ public function dataArrayDestructuring(): array '$secondStringArrayList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayList', ], [ @@ -1097,43 +1091,43 @@ public function dataArrayDestructuring(): array '$fourthStringArrayList', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeachList', ], [ - 'string', + 'lowercase-string&uppercase-string', '$dateArray[\'Y\']', ], [ - 'string', + 'lowercase-string&uppercase-string', '$dateArray[\'m\']', ], [ @@ -1141,7 +1135,7 @@ public function dataArrayDestructuring(): array '$dateArray[\'d\']', ], [ - 'string', + 'lowercase-string&uppercase-string', '$intArrayForRewritingFirstElement[0]', ], [ @@ -1149,7 +1143,7 @@ public function dataArrayDestructuring(): array '$intArrayForRewritingFirstElement[1]', ], [ - 'stdClass', + 'ArrayAccess&stdClass', '$obj', ], [ @@ -1211,24 +1205,20 @@ public function dataArrayDestructuring(): array ]; } - /** - * @dataProvider dataArrayDestructuring - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayDestructuring')] public function testArrayDestructuring( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-destructuring.php', $description, - $expression + $expression, ); } - public function dataParameterTypes(): array + public static function dataParameterTypes(): array { return [ [ @@ -1284,7 +1274,7 @@ public function dataParameterTypes(): array '$callable', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$variadicStrings', ], [ @@ -1294,24 +1284,20 @@ public function dataParameterTypes(): array ]; } - /** - * @dataProvider dataParameterTypes - * @param string $typeClass - * @param string $expression - */ + #[DataProvider('dataParameterTypes')] public function testTypehints( string $typeClass, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/typehints.php', $typeClass, - $expression + $expression, ); } - public function dataAnonymousFunctionParameterTypes(): array + public static function dataAnonymousFunctionParameterTypes(): array { return [ [ @@ -1357,24 +1343,20 @@ public function dataAnonymousFunctionParameterTypes(): array ]; } - /** - * @dataProvider dataAnonymousFunctionParameterTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataAnonymousFunctionParameterTypes')] public function testAnonymousFunctionTypehints( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/typehints-anonymous-function.php', $description, - $expression + $expression, ); } - public function dataVarAnnotations(): array + public static function dataVarAnnotations(): array { return [ [ @@ -1418,11 +1400,11 @@ public function dataVarAnnotations(): array '$callable', ], [ - 'callable(int, ...string): void', + 'callable(int, string ...): void', '$callableWithTypes', ], [ - 'Closure(int, ...string): void', + 'Closure(int, string ...): void', '$closureWithTypes', ], [ @@ -1440,14 +1422,10 @@ public function dataVarAnnotations(): array ]; } - /** - * @dataProvider dataVarAnnotations - * @param string $description - * @param string $expression - */ + #[DataProvider('dataVarAnnotations')] public function testVarAnnotations( string $description, - string $expression + string $expression, ): void { $this->assertTypes( @@ -1456,11 +1434,11 @@ public function testVarAnnotations( $expression, 'die', [], - false + false, ); } - public function dataCasts(): array + public static function dataCasts(): array { return [ [ @@ -1560,31 +1538,31 @@ public function dataCasts(): array '(float) $str', ], [ - 'array(\'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'foo\' => TypesNamespaceCasts\Foo, \'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'int\' => int, \'\' . "\0" . \'*\' . "\0" . \'protectedInt\' => int, \'publicInt\' => int, \'\' . "\0" . \'TypesNamespaceCasts\\\\Bar\' . "\0" . \'barProperty\' => TypesNamespaceCasts\Bar)', + "array{'\0TypesNamespaceCasts\\Foo\0foo': TypesNamespaceCasts\\Foo, '\0TypesNamespaceCasts\\Foo\0int': int, '\0*\0protectedInt': int, publicInt: int, '\0TypesNamespaceCasts\\Bar\0barProperty': TypesNamespaceCasts\\Bar}", '(array) $foo', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '(array) [1, 2, 3]', ], [ - 'array(1)', + 'array{1}', '(array) 1', ], [ - 'array(1.0)', + 'array{1.0}', '(array) 1.0', ], [ - 'array(true)', + 'array{true}', '(array) true', ], [ - 'array(\'blabla\')', + 'array{\'blabla\'}', '(array) "blabla"', ], [ - 'array(int)', + 'array{int}', '(array) $castedInteger', ], [ @@ -1598,56 +1576,20 @@ public function dataCasts(): array ]; } - /** - * @dataProvider dataCasts - * @param string $desciptiion - * @param string $expression - */ + #[DataProvider('dataCasts')] public function testCasts( string $desciptiion, - string $expression + 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 + $expression, ); } - public function dataDeductedTypes(): array + public static function dataDeductedTypes(): array { return [ [ @@ -1691,7 +1633,7 @@ public function dataDeductedTypes(): array '$newStatic', ], [ - 'array()', + 'array{}', '$arrayLiteral', ], [ @@ -1723,7 +1665,7 @@ public function dataDeductedTypes(): array 'self::STRING_CONSTANT', ], [ - 'array()', + 'array{}', 'self::ARRAY_CONSTANT', ], [ @@ -1747,7 +1689,7 @@ public function dataDeductedTypes(): array '$foo::STRING_CONSTANT', ], [ - 'array()', + 'array{}', '$foo::ARRAY_CONSTANT', ], [ @@ -1761,25 +1703,21 @@ public function dataDeductedTypes(): array ]; } - /** - * @dataProvider dataDeductedTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataDeductedTypes')] public function testDeductedTypes( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/function-definitions.php'; $this->assertTypes( __DIR__ . '/data/deducted-types.php', $description, - $expression + $expression, ); } - public function dataProperties(): array + public static function dataProperties(): array { return [ [ @@ -1807,7 +1745,7 @@ public function dataProperties(): array '$this->arrayPropertyOne', ], [ - 'array', + 'array', '$this->arrayPropertyOther', ], [ @@ -1863,7 +1801,7 @@ public function dataProperties(): array '$this->resource', ], [ - 'array', + 'mixed', '$this->yetAnotherAnotherMixedParameter', ], [ @@ -1903,30 +1841,26 @@ public function dataProperties(): array '$this->overriddenReadOnlyProperty', ], [ - 'string', + PHP_VERSION_ID < 80100 ? 'string' : 'DOMElement|null', '$this->documentElement', ], ]; } - /** - * @dataProvider dataProperties - * @param string $description - * @param string $expression - */ + #[DataProvider('dataProperties')] public function testProperties( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/properties.php', $description, - $expression + $expression, ); } - public function dataBinaryOperations(): array + public static function dataBinaryOperations(): array { $typeCallback = static function ($value): string { if (is_int($value)) { @@ -1939,7 +1873,7 @@ public function dataBinaryOperations(): array return (new ConstantStringType($value))->describe(VerbosityLevel::precise()); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); }; return [ @@ -2082,7 +2016,7 @@ public function dataBinaryOperations(): array '1.2 ** 1.4', ], [ - $typeCallback(3.2 % 2.4), + '1', '3.2 % 2.4', ], [ @@ -2115,7 +2049,7 @@ public function dataBinaryOperations(): array '1 ** 1.4', ], [ - $typeCallback(3 % 2.4), + '1', '3 % 2.4', ], [ @@ -2164,7 +2098,7 @@ public function dataBinaryOperations(): array '$integer ** $integer', ], [ - $typeCallback(3.2 % 2), + '1', '3.2 % 2', ], [ @@ -2302,15 +2236,15 @@ public function dataBinaryOperations(): array 'false ? 1 : 2', ], [ - '12|non-empty-string', + '12|non-falsy-string', '$string ?: 12', ], [ - '12|non-empty-string', + '12|non-falsy-string', '$stringOrNull ?: 12', ], [ - '12|non-empty-string', + '12|non-falsy-string', '@$stringOrNull ?: 12', ], [ @@ -2354,11 +2288,11 @@ public function dataBinaryOperations(): array '$line', ], [ - (new ConstantStringType(__DIR__ . '/data'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$dir', ], [ - (new ConstantStringType(__DIR__ . '/data/binary.php'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$file', ], [ @@ -2382,7 +2316,7 @@ public function dataBinaryOperations(): array 'min([1, 2, 3])', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', 'min([1, 2, 3], [4, 5, 5])', ], [ @@ -2398,25 +2332,53 @@ public function dataBinaryOperations(): array 'min(0, ...[1, 2, 3])', ], [ - 'array(5, 6, 9)', + 'array{5, 6, 9}', 'max([1, 10, 8], [5, 6, 9])', ], [ - 'array(1, 1, 1, 1)', + 'array{1, 1, 1, 1}', 'max(array(2, 2, 2), array(1, 1, 1, 1))', ], [ 'array', 'max($arrayOfUnknownIntegers, $arrayOfUnknownIntegers)', ], - /*[ - 'array(1, 1, 1, 1)', + [ + 'array{1, 1, 1, 1}', 'max(array(2, 2, 2), 5, array(1, 1, 1, 1))', ], + [ + 'array{int, int, int}', + 'max($arrayOfIntegers, 5)', + ], [ 'array', + 'max($arrayOfUnknownIntegers, 5)', + ], + [ + 'array|int', // could be array 'max($arrayOfUnknownIntegers, $integer, $arrayOfUnknownIntegers)', - ],*/ + ], + [ + 'array', + 'max($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min($arrayOfIntegers, 5)', + ], + [ + '5', + 'min($arrayOfUnknownIntegers, 5)', + ], + [ + '1|2', + 'min($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min(array(2, 2, 2), 5, array(1, 1, 1, 1))', + ], [ '1.1', 'min(...[1.1, 2.2, 3.3])', @@ -2482,11 +2444,11 @@ public function dataBinaryOperations(): array 'min(1, 2.2, 3.3)', ], [ - 'non-empty-string', + 'non-falsy-string', '"Hello $world"', ], [ - 'non-empty-string', + 'non-falsy-string', '$string .= "str"', ], [ @@ -2622,31 +2584,31 @@ public function dataBinaryOperations(): array '!isset($foo)', ], [ - 'bool', + 'false', 'empty($foo)', ], [ - 'bool', + 'true', '!empty($foo)', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers + $arrayOfIntegers', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers += $arrayOfIntegers', ], [ - 'array(0 => 1, 1 => 1, 2 => 1, 3 => 1|2, 4 => 1|3, ?5 => 2|3, ?6 => 3)', + 'array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}', '$conditionalArray + $unshiftedConditionalArray', ], [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', + 'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', '$unshiftedConditionalArray + $conditionalArray', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers += ["foo"]', ], [ @@ -2658,73 +2620,9 @@ public function dataBinaryOperations(): array '@count($arrayOfIntegers)', ], [ - 'array(int, int, int)', + '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++', @@ -2758,15 +2656,15 @@ public function dataBinaryOperations(): array '$preIncArray[3]', ], [ - 'array(1 => 1, 2 => 2)', + 'array{1: 1, 2: 2}', '$preIncArray', ], [ - 'array(0 => 1, 2 => 3)', + 'array{0: 1, 2: 3}', '$postIncArray', ], [ - 'array(0 => array(1 => array(2 => 3)), 4 => array(5 => array(6 => 7)))', + 'array{0: array{1: array{2: 3}}, 4: array{5: array{6: 7}}}', '$anotherPostIncArray', ], [ @@ -2782,7 +2680,7 @@ public function dataBinaryOperations(): array 'count($appendingToArrayInBranches)', ], [ - '3|4|5', + '3|5', 'count($conditionalArray)', ], [ @@ -2806,9 +2704,49 @@ public function dataBinaryOperations(): array '$mixed - $mixed', ], [ - '*ERROR*', + 'array', '$mixed + []', ], + [ + 'array|int', + '$intOrArray + $intOrArray', + ], + [ + 'float|int', + '$intOrFloat + $intOrFloat', + ], + [ + 'array|float', + '$floatOrArray + $floatOrArray', + ], + [ + 'array|bool|float|int|string', + '$plusable + $plusable', + ], + [ + 'array', + '$mixedNoFloat + []', + ], + [ + '(float|int)', + '$mixedNoFloat + 5', + ], + [ + '(float|int)', + '$mixedNoInt + 5', + ], + [ + '*ERROR*', + '$mixedNoArray + []', + ], + [ + '*ERROR*', + '$mixedNoArrayOrInt + []', + ], + [ + '*ERROR*', + '$integer + []', + ], [ '124', '1 + "123"', @@ -2826,11 +2764,11 @@ public function dataBinaryOperations(): array '1 + "blabla"', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '[1, 2, 3] + [4, 5, 6]', ], [ - 'array', + 'non-empty-array', '$arrayOfUnknownIntegers + [1, 2, 3]', ], [ @@ -2854,9 +2792,17 @@ public function dataBinaryOperations(): array '5 & 3', ], [ - 'int', + 'int<0, 3>', '$integer & 3', ], + [ + 'int<0, 7>', + '7 & $integer', + ], + [ + 'int', + '$integer & $integer', + ], [ '\'x\'', '"x" & "y"', @@ -2906,7 +2852,7 @@ public function dataBinaryOperations(): array '$integer ^ 3', ], [ - '\'' . "\x01" . '\'', + '"\001"', '"x" ^ "y"', ], [ @@ -2922,7 +2868,7 @@ public function dataBinaryOperations(): array '"5" ^ 3', ], [ - 'int', + 'int<0, 3>', '$integer &= 3', ], [ @@ -2966,7 +2912,7 @@ public function dataBinaryOperations(): array '$fooString[4]', ], [ - 'string', + "''|'f'|'o'", '$fooString[$integer]', ], [ @@ -2978,31 +2924,31 @@ public function dataBinaryOperations(): array '"$fooString bar"', ], [ - '*ERROR*', + 'non-falsy-string', '"$std bar"', ], [ - 'array<\'foo\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'foo\'|int|stdClass>', '$arrToPush', ], [ - 'array<\'foo\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'foo\'|int|stdClass>', '$arrToPush2', ], [ - 'array(0 => \'lorem\', 1 => 5, \'foo\' => stdClass, 2 => \'test\')', + 'array{0: \'lorem\', 1: 5, foo: stdClass, 2: \'test\'}', '$arrToUnshift', ], [ - 'array<\'lorem\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'lorem\'|int|stdClass>', '$arrToUnshift2', ], [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', + 'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', '$unshiftedConditionalArray', ], [ - 'array(\'dirname\' => string, \'basename\' => string, \'filename\' => string, ?\'extension\' => string)', + 'array{dirname?: string, basename: string, extension?: string, filename: string}', 'pathinfo($string)', ], [ @@ -3018,19 +2964,19 @@ public function dataBinaryOperations(): array '$string--', ], [ - 'string', + '(float|int|string)', '++$string', ], [ - 'string', + '(float|int|string)', '--$string', ], [ - 'string', + '(float|int|string)', '$incrementedString', ], [ - 'string', + '(float|int|string)', '$decrementedString', ], [ @@ -3046,7 +2992,7 @@ public function dataBinaryOperations(): array '++$fooString', ], [ - '\'foo\'', + '\'fon\'', '--$fooString', ], [ @@ -3054,23 +3000,23 @@ public function dataBinaryOperations(): array '$incrementedFooString', ], [ - '\'foo\'', + '\'fon\'', '$decrementedFooString', ], [ - 'literal-string&non-empty-string', + "'barbar'|'barfoo'|'foobar'|'foofoo'", '$conditionalString . $conditionalString', ], [ - 'literal-string&non-empty-string', + "'baripsum'|'barlorem'|'fooipsum'|'foolorem'", '$conditionalString . $anotherConditionalString', ], [ - 'literal-string&non-empty-string', + "'ipsumbar'|'ipsumfoo'|'lorembar'|'loremfoo'", '$anotherConditionalString . $conditionalString', ], [ - '6|7|8', + '6|8', 'count($conditionalArray) + count($array)', ], [ @@ -3106,11 +3052,11 @@ public function dataBinaryOperations(): array 'in_array(\'baz\', [\'foo\', \'bar\'], true)', ], [ - 'array(2, 3)', + 'array{2, 3}', '$arrToShift', ], [ - 'array(1, 2)', + 'array{1, 2}', '$arrToPop', ], [ @@ -3141,14 +3087,6 @@ public function dataBinaryOperations(): array 'bool', 'array_key_exists(\'foo\', $generalArray)', ], - [ - PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle', - 'curl_init()', - ], - [ - PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false', - 'curl_init($string)', - ], [ 'string', 'sprintf($string, $string, 1)', @@ -3158,23 +3096,31 @@ public function dataBinaryOperations(): array "sprintf('%s %s', 'foo', 'bar')", ], [ - 'array()|array(0 => \'password\'|\'username\', ?1 => \'password\')', + 'array{}|array{\'password\'}|array{0: \'username\', 1?: \'password\'}', '$coalesceArray', ], [ - 'array', + 'array{1, 2, 3}', '$arrayToBeUnset', ], [ - 'array', + 'array{1, 2, 3}', '$arrayToBeUnset2', ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset3', + ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset4', + ], [ 'array', '$shiftedNonEmptyArray', ], [ - 'array&nonEmpty', + 'non-empty-array', '$unshiftedArray', ], [ @@ -3182,7 +3128,7 @@ public function dataBinaryOperations(): array '$poppedNonEmptyArray', ], [ - 'array&nonEmpty', + 'non-empty-array', '$pushedArray', ], [ @@ -3190,7 +3136,7 @@ public function dataBinaryOperations(): array '$simpleXMLReturningXML', ], [ - 'non-empty-string', + 'non-falsy-string', '$xmlString', ], [ @@ -3198,42 +3144,38 @@ public function dataBinaryOperations(): array '$simpleXMLWritingXML', ], [ - 'array', + 'array|null', '$simpleXMLRightXpath', ], [ - 'array|false', + 'array|false|null', '$simpleXMLWrongXpath', ], [ - 'array|false', + 'array|false|null', '$simpleXMLUnknownXpath', ], [ - 'array|false', + 'array|false|null', '$namespacedXpath', ], ]; } - /** - * @dataProvider dataBinaryOperations - * @param string $description - * @param string $expression - */ + #[DataProvider('dataBinaryOperations')] public function testBinaryOperations( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/binary.php', $description, - $expression + $expression, ); } - public function dataVarStatementAnnotation(): array + public static function dataVarStatementAnnotation(): array { return [ [ @@ -3243,24 +3185,20 @@ public function dataVarStatementAnnotation(): array ]; } - /** - * @dataProvider dataVarStatementAnnotation - * @param string $description - * @param string $expression - */ + #[DataProvider('dataVarStatementAnnotation')] public function testVarStatementAnnotation( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/var-stmt-annotation.php', $description, - $expression + $expression, ); } - public function dataCloneOperators(): array + public static function dataCloneOperators(): array { return [ [ @@ -3270,24 +3208,20 @@ public function dataCloneOperators(): array ]; } - /** - * @dataProvider dataCloneOperators - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCloneOperators')] public function testCloneOperators( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/clone.php', $description, - $expression + $expression, ); } - public function dataLiteralArrays(): array + public static function dataLiteralArrays(): array { return [ [ @@ -3315,7 +3249,7 @@ public function dataLiteralArrays(): array '$integers[0] >= $integers[1] - 1', ], [ - 'array(\'foo\' => array(\'foo\' => array(\'foo\' => \'bar\')), \'bar\' => array(), \'baz\' => array(\'lorem\' => array()))', + 'array{foo: array{foo: array{foo: \'bar\'}}, bar: array{}, baz: array{lorem: array{}}}', '$nestedArray', ], [ @@ -3325,24 +3259,20 @@ public function dataLiteralArrays(): array ]; } - /** - * @dataProvider dataLiteralArrays - * @param string $description - * @param string $expression - */ + #[DataProvider('dataLiteralArrays')] public function testLiteralArrays( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/literal-arrays.php', $description, - $expression + $expression, ); } - public function dataLiteralArraysKeys(): array + public static function dataLiteralArraysKeys(): array { define('STRING_ONE', '1'); define('INT_ONE', 1); @@ -3398,31 +3328,27 @@ public function dataLiteralArraysKeys(): array "'BooleansArray'", ], [ - 'int|string', + '(int|string)', "'UnknownConstantArray'", ], ]; } - /** - * @dataProvider dataLiteralArraysKeys - * @param string $description - * @param string $evaluatedPointExpressionType - */ + #[DataProvider('dataLiteralArraysKeys')] public function testLiteralArraysKeys( string $description, - string $evaluatedPointExpressionType + string $evaluatedPointExpressionType, ): void { $this->assertTypes( __DIR__ . '/data/literal-arrays-keys.php', $description, '$key', - $evaluatedPointExpressionType + $evaluatedPointExpressionType, ); } - public function dataStringArrayAccess(): array + public static function dataStringArrayAccess(): array { return [ [ @@ -3448,24 +3374,20 @@ public function dataStringArrayAccess(): array ]; } - /** - * @dataProvider dataStringArrayAccess - * @param string $description - * @param string $expression - */ + #[DataProvider('dataStringArrayAccess')] public function testStringArrayAccess( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/string-array-access.php', $description, - $expression + $expression, ); } - public function dataTypeFromFunctionPhpDocs(): array + public static function dataTypeFromFunctionPhpDocs(): array { return [ [ @@ -3497,7 +3419,7 @@ public function dataTypeFromFunctionPhpDocs(): array '$arrayParameterOne', ], [ - 'array', + 'array', '$arrayParameterOther', ], [ @@ -3611,7 +3533,7 @@ public function dataTypeFromFunctionPhpDocs(): array ]; } - public function dataTypeFromFunctionFunctionPhpDocs(): array + public static function dataTypeFromFunctionFunctionPhpDocs(): array { return [ [ @@ -3625,26 +3547,22 @@ public function dataTypeFromFunctionFunctionPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionFunctionPhpDocs - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionFunctionPhpDocs')] public function testTypeFromFunctionPhpDocs( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs.php', $description, - $expression + $expression, ); } - public function dataTypeFromFunctionPrefixedPhpDocs(): array + public static function dataTypeFromFunctionPrefixedPhpDocs(): array { return [ [ @@ -3654,45 +3572,52 @@ public function dataTypeFromFunctionPrefixedPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionPrefixedPhpDocs')] public function testTypeFromFunctionPhpDocsPsalmPrefix( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs-psalmPrefix.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs-psalmPrefix.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionPrefixedPhpDocs')] public function testTypeFromFunctionPhpDocsPhpstanPrefix( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php', $description, - $expression + $expression, + ); + } + + #[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 + public static function dataTypeFromMethodPhpDocs(): array { return [ [ @@ -3834,35 +3759,26 @@ public function dataTypeFromMethodPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocs( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/methodPhpDocs.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsPsalmPrefix( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); @@ -3876,21 +3792,19 @@ public function testTypeFromMethodPhpDocsPsalmPrefix( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-psalmPrefix.php', $description, - $expression + $expression, ); } /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression * @param bool $replaceClass = true */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsPhpstanPrefix( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); @@ -3904,21 +3818,39 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-phpstanPrefix.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[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 + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithTrait)', $description); @@ -3932,49 +3864,39 @@ public function testTypeFromTraitPhpDocs( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-trait.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( string $description, string $expression, - bool $replaceClass = true + 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('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); $description = str_replace('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; + $description = 'MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly'; } } $this->assertTypes( __DIR__ . '/data/method-phpDocs-inheritdoc-without-curly-braces.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromRecursiveTraitPhpDocs( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); @@ -3988,11 +3910,11 @@ public function testTypeFromRecursiveTraitPhpDocs( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-recursiveTrait.php', $description, - $expression + $expression, ); } - public function dataTypeFromTraitPhpDocsInSameFile(): array + public static function dataTypeFromTraitPhpDocsInSameFile(): array { return [ [ @@ -4002,34 +3924,25 @@ public function dataTypeFromTraitPhpDocsInSameFile(): array ]; } - /** - * @dataProvider dataTypeFromTraitPhpDocsInSameFile - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTypeFromTraitPhpDocsInSameFile')] public function testTypeFromTraitPhpDocsInSameFile( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/methodPhpDocs-traitInSameFileAsClass.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsInheritDoc( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { if ($replaceClass) { @@ -4043,21 +3956,16 @@ public function testTypeFromMethodPhpDocsInheritDoc( $this->assertTypes( __DIR__ . '/data/method-phpDocs-inheritdoc.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsImplicitInheritance( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { if ($replaceClass) { @@ -4071,7 +3979,7 @@ public function testTypeFromMethodPhpDocsImplicitInheritance( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-implicitInheritance.php', $description, - $expression + $expression, ); } @@ -4079,12 +3987,12 @@ public function testNotSwitchInstanceof(): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-not.php', - '*ERROR*', - '$foo' + '*NEVER*', + '$foo', ); } - public function dataSwitchInstanceOf(): array + public static function dataSwitchInstanceOf(): array { return [ [ @@ -4102,41 +4010,33 @@ public function dataSwitchInstanceOf(): array ]; } - /** - * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSwitchInstanceOf')] public function testSwitchInstanceof( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSwitchInstanceOf')] public function testSwitchInstanceofTruthy( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-truthy.php', $description, - $expression + $expression, ); } - public function dataSwitchGetClass(): array + public static function dataSwitchGetClass(): array { return [ [ @@ -4152,27 +4052,22 @@ public function dataSwitchGetClass(): array ]; } - /** - * @dataProvider dataSwitchGetClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataSwitchGetClass')] public function testSwitchGetClass( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/switch-get-class.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataSwitchInstanceOfFallthrough(): array + public static function dataSwitchInstanceOfFallthrough(): array { return [ [ @@ -4182,24 +4077,20 @@ public function dataSwitchInstanceOfFallthrough(): array ]; } - /** - * @dataProvider dataSwitchInstanceOfFallthrough - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSwitchInstanceOfFallthrough')] public function testSwitchInstanceOfFallthrough( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-fallthrough.php', $description, - $expression + $expression, ); } - public function dataSwitchTypeElimination(): array + public static function dataSwitchTypeElimination(): array { return [ [ @@ -4209,24 +4100,20 @@ public function dataSwitchTypeElimination(): array ]; } - /** - * @dataProvider dataSwitchTypeElimination - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSwitchTypeElimination')] public function testSwitchTypeElimination( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-type-elimination.php', $description, - $expression + $expression, ); } - public function dataOverwritingVariable(): array + public static function dataOverwritingVariable(): array { return [ [ @@ -4247,27 +4134,22 @@ public function dataOverwritingVariable(): array ]; } - /** - * @dataProvider dataOverwritingVariable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpressionType - */ + #[DataProvider('dataOverwritingVariable')] public function testOverwritingVariable( string $description, string $expression, - string $evaluatedPointExpressionType + string $evaluatedPointExpressionType, ): void { $this->assertTypes( __DIR__ . '/data/overwritingVariable.php', $description, $expression, - $evaluatedPointExpressionType + $evaluatedPointExpressionType, ); } - public function dataNegatedInstanceof(): array + public static function dataNegatedInstanceof(): array { return [ [ @@ -4317,24 +4199,20 @@ public function dataNegatedInstanceof(): array ]; } - /** - * @dataProvider dataNegatedInstanceof - * @param string $description - * @param string $expression - */ + #[DataProvider('dataNegatedInstanceof')] public function testNegatedInstanceof( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/negated-instanceof.php', $description, - $expression + $expression, ); } - public function dataAnonymousFunction(): array + public static function dataAnonymousFunction(): array { return [ [ @@ -4342,7 +4220,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$arr', ], [ @@ -4356,24 +4234,20 @@ public function dataAnonymousFunction(): array ]; } - /** - * @dataProvider dataAnonymousFunction - * @param string $description - * @param string $expression - */ + #[DataProvider('dataAnonymousFunction')] public function testAnonymousFunction( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-function.php', $description, - $expression + $expression, ); } - public function dataForeachArrayType(): array + public static function dataForeachArrayType(): array { return [ [ @@ -4453,7 +4327,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', - 'array&nonEmpty', + 'non-empty-array', '$list', ], [ @@ -4473,7 +4347,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'ForeachWithGenericsPhpDoc\Bar|ForeachWithGenericsPhpDoc\Foo', + 'ForeachWithGenericsPhpDocIterable\Bar|ForeachWithGenericsPhpDocIterable\Foo', '$key', ], [ @@ -4483,7 +4357,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', + 'float|ForeachIterableWithComplexValueType\Foo', '$value', ], [ @@ -4494,26 +4368,21 @@ public function dataForeachArrayType(): array ]; } - /** - * @dataProvider dataForeachArrayType - * @param string $file - * @param string $description - * @param string $expression - */ + #[DataProvider('dataForeachArrayType')] public function testForeachArrayType( string $file, string $description, - string $expression + string $expression, ): void { $this->assertTypes( $file, $description, - $expression + $expression, ); } - public function dataOverridingSpecifiedType(): array + public static function dataOverridingSpecifiedType(): array { return [ [ @@ -4524,26 +4393,21 @@ public function dataOverridingSpecifiedType(): array ]; } - /** - * @dataProvider dataOverridingSpecifiedType - * @param string $file - * @param string $description - * @param string $expression - */ + #[DataProvider('dataOverridingSpecifiedType')] public function testOverridingSpecifiedType( string $file, string $description, - string $expression + string $expression, ): void { $this->assertTypes( $file, $description, - $expression + $expression, ); } - public function dataForeachObjectType(): array + public static function dataForeachObjectType(): array { return [ [ @@ -4585,29 +4449,23 @@ public function dataForeachObjectType(): array ]; } - /** - * @dataProvider dataForeachObjectType - * @param string $file - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataForeachObjectType')] public function testForeachObjectType( string $file, string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( $file, $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataArrayFunctions(): array + public static function dataArrayFunctions(): array { return [ [ @@ -4615,7 +4473,7 @@ public function dataArrayFunctions(): array '$integers[0]', ], [ - 'array(string, string, string)', + 'array{string, string, string}', '$mappedStrings', ], [ @@ -4627,9 +4485,17 @@ public function dataArrayFunctions(): array '$filteredIntegers[0]', ], [ - '123', + '*ERROR*', '$filteredMixed[0]', ], + [ + '123', + '$filteredMixed[1]', + ], + [ + 'non-empty-array<0|1|2, 1|2|3>', + '$uniquedIntegers', + ], [ '1|2|3', '$uniquedIntegers[1]', @@ -4659,7 +4525,7 @@ public function dataArrayFunctions(): array '$reducedToInt', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', 'array_change_key_case($integers)', ], [ @@ -4667,15 +4533,15 @@ public function dataArrayFunctions(): array 'array_combine($array, $array2)', ], [ - 'array(1 => 2)', + 'array{1: 2}', 'array_combine([1], [2])', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', 'array_combine([1, 2], [3])', ], [ - 'array(\'a\' => \'d\', \'b\' => \'e\', \'c\' => \'f\')', + 'array{a: \'d\', b: \'e\', c: \'f\'}', 'array_combine([\'a\', \'b\', \'c\'], [\'d\', \'e\', \'f\'])', ], [ @@ -4731,16 +4597,16 @@ public function dataArrayFunctions(): array 'array_intersect_assoc($integers, [])', ], [ - 'array<0|1|2, 1|2|3>', + 'array{}', 'array_intersect_key($integers, [])', ], [ - 'array', + 'array{1, 2, 3}|array{4, 5, 6}', 'array_intersect_key(...[$integers, [4, 5, 6]])', ], [ 'array', - 'array_intersect_key(...$generalIntegersInAnotherArray, [])', + 'array_intersect_key(...$generalIntegersInAnotherArray)', ], [ 'array<0|1|2, 1|2|3>', @@ -4767,19 +4633,19 @@ public function dataArrayFunctions(): array 'array_uintersect($integers, [])', ], [ - 'array(1, 1, 1, 1, 1)', + 'array{1, 1, 1, 1, 1}', '$filledIntegers', ], [ - 'array()', + 'array{}', '$emptyFilled', ], [ - 'array(1)', + 'array{1}', '$filledIntegersWithKeys', ], [ - 'array&nonEmpty', + 'non-empty-list<\'foo\'>', '$filledNonEmptyArray', ], [ @@ -4791,39 +4657,39 @@ public function dataArrayFunctions(): array '$filledNegativeConstAlwaysFalse', ], [ - 'array|false', + PHP_VERSION_ID < 80000 ? 'list<1>|false' : 'list<1>', '$filledByMaybeNegativeRange', ], [ - 'array&nonEmpty', + 'non-empty-list<1>', '$filledByPositiveRange', ], [ - 'array(1, 2)', + 'array{1, 2}', 'array_keys($integerKeys)', ], [ - 'array(\'foo\', \'bar\')', + 'array{\'foo\', \'bar\'}', 'array_keys($stringKeys)', ], [ - 'array(\'foo\', 1)', + 'array{\'foo\', 1}', 'array_keys($stringOrIntegerKeys)', ], [ - 'array', + 'list', 'array_keys($generalStringKeys)', ], [ - 'array(\'foo\', stdClass)', + 'array{\'foo\', stdClass}', 'array_values($integerKeys)', ], [ - 'array', + 'list', 'array_values($generalStringKeys)', ], [ - 'array&nonEmpty', + 'array{foo: stdClass, 0: stdClass}', 'array_merge($stringOrIntegerKeys)', ], [ @@ -4831,23 +4697,36 @@ public function dataArrayFunctions(): array 'array_merge($generalStringKeys, $generalDateTimeValues)', ], [ - 'array&nonEmpty', + 'non-empty-array<1|string, int|stdClass>', 'array_merge($generalStringKeys, $stringOrIntegerKeys)', ], [ - 'array&nonEmpty', + 'non-empty-array<1|string, int|stdClass>', 'array_merge($stringOrIntegerKeys, $generalStringKeys)', ], [ - 'array&nonEmpty', + 'array{foo: stdClass, bar: stdClass, 0: stdClass}', 'array_merge($stringKeys, $stringOrIntegerKeys)', ], [ - 'array&nonEmpty', + "array{foo: 'foo', 0: stdClass, bar: stdClass}", 'array_merge($stringOrIntegerKeys, $stringKeys)', ], [ - 'array&nonEmpty', + '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))', ], [ @@ -4859,23 +4738,23 @@ public function dataArrayFunctions(): array '$mergedInts', ], [ - 'array(5 => \'banana\', 6 => \'banana\', 7 => \'banana\', 8 => \'banana\', 9 => \'banana\', 10 => \'banana\')', + 'array{5: \'banana\', 6: \'banana\', 7: \'banana\', 8: \'banana\', 9: \'banana\', 10: \'banana\'}', 'array_fill(5, 6, \'banana\')', ], [ - 'array&nonEmpty', + 'non-empty-list<\'apple\'>', 'array_fill(0, 101, \'apple\')', ], [ - 'array(-2 => \'pear\', 0 => \'pear\', 1 => \'pear\', 2 => \'pear\')', + 'array{-2: \'pear\', 0: \'pear\', 1: \'pear\', 2: \'pear\'}', 'array_fill(-2, 4, \'pear\')', ], [ - 'array&nonEmpty', + 'non-empty-array', 'array_fill($integer, 2, new \stdClass())', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'array|false' : 'array', 'array_fill(2, $integer, new \stdClass())', ], [ @@ -4883,7 +4762,7 @@ public function dataArrayFunctions(): array 'array_fill_keys($generalStringKeys, new \stdClass())', ], [ - 'array(\'foo\' => \'banana\', 5 => \'banana\', 10 => \'banana\', \'bar\' => \'banana\')', + 'array{foo: \'banana\', 5: \'banana\', 10: \'banana\', bar: \'banana\'}', 'array_fill_keys([\'foo\', 5, 10, \'bar\'], \'banana\')', ], [ @@ -4903,11 +4782,11 @@ public function dataArrayFunctions(): array '$unknownArray', ], [ - 'array(\'foo\' => \'banana\', \'bar\' => \'banana\', ?\'baz\' => \'banana\', ?\'lorem\' => \'banana\')', + 'array{foo: \'banana\', bar: \'banana\', baz: \'banana\', lorem: \'banana\'}|array{foo: \'banana\', bar: \'banana\'}', 'array_fill_keys($conditionalArray, \'banana\')', ], [ - 'array(\'foo\' => stdClass, \'bar\' => stdClass, ?\'baz\' => stdClass, ?\'lorem\' => stdClass)', + 'array{foo: stdClass, bar: stdClass, baz: stdClass, lorem: stdClass}|array{foo: stdClass, bar: stdClass}', 'array_map(function (): \stdClass {}, $conditionalKeysArray)', ], [ @@ -4915,7 +4794,7 @@ public function dataArrayFunctions(): array 'array_pop($stringKeys)', ], [ - 'array&hasOffset(\'baz\')', + 'non-empty-array&hasOffsetValue(\'baz\', stdClass)', '$stdClassesWithIsset', ], [ @@ -4943,11 +4822,11 @@ public function dataArrayFunctions(): array 'array_shift([])', ], [ - 'array(null, \'\', 1)', + 'array{null, \'\', 1}', '$constantArrayWithFalseyValues', ], [ - 'array(2 => 1)', + 'array{2: 1}', '$constantTruthyValues', ], [ @@ -4955,7 +4834,7 @@ public function dataArrayFunctions(): array '$falsey', ], [ - 'array()', + 'array{}', 'array_filter($falsey)', ], [ @@ -4967,15 +4846,15 @@ public function dataArrayFunctions(): array 'array_filter($withFalsey)', ], [ - 'array(\'a\' => 1)', + 'array{a: 1}', 'array_filter($union)', ], [ - 'array(?0 => true, ?1 => int|int<1, max>)', + 'array{0?: true, 1?: int|int<1, max>}', 'array_filter($withPossiblyFalsey)', ], [ - '(array|null)', + PHP_VERSION_ID < 80000 ? '(array|null)' : 'array', 'array_filter($mixed)', ], [ @@ -5003,7 +4882,7 @@ public function dataArrayFunctions(): array 'array_search(9, $generalStringKeys)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(999, $integer, true)', ], [ @@ -5051,19 +4930,19 @@ public function dataArrayFunctions(): array 'array_search(\'id\', $generalIntegerOrStringKeysMixedValues, true)', ], [ - 'int|string|false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? $generalIntegerOrStringKeys : false, true)', ], [ - 'false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? [] : false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false)', ], [ @@ -5159,55 +5038,55 @@ public function dataArrayFunctions(): array 'array_slice($unknownArray, -2, 1, true)', ], [ - 'array(0 => bool, 1 => int, 2 => \'\', \'a\' => 0)', + 'array{0: bool, 1: int, 2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 0)', ], [ - 'array(0 => int, 1 => \'\', \'a\' => 0)', + 'array{0: int, 1: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 1)', ], [ - 'array(1 => int, 2 => \'\', \'a\' => 0)', + 'array{1: int, 2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 1, null, true)', ], [ - 'array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 2, 3)', ], [ - 'array(2 => \'\', \'a\' => 0)', + 'array{2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 2, 3, true)', ], [ - 'array(int, \'\')', + 'array{int, \'\'}', 'array_slice($withPossiblyFalsey, 1, -1)', ], [ - 'array(1 => int, 2 => \'\')', + 'array{1: int, 2: \'\'}', 'array_slice($withPossiblyFalsey, 1, -1, true)', ], [ - 'array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}', 'array_slice($withPossiblyFalsey, -2, null)', ], [ - 'array(2 => \'\', \'a\' => 0)', + 'array{2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, -2, null, true)', ], [ - 'array(\'baz\' => \'qux\')|array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}|array{baz: \'qux\'}', 'array_slice($unionArrays, 1)', ], [ - 'array(\'a\' => 0)|array(\'baz\' => \'qux\')', + 'array{a: 0}|array{baz: \'qux\'}', 'array_slice($unionArrays, -1, null, true)', ], [ - 'array(0 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 2 => \'quux\', \'quuz\' => \'corge\', 3 => \'grault\')', + 'array{0: \'foo\', 1: \'bar\', baz: \'qux\', 2: \'quux\', quuz: \'corge\', 3: \'grault\'}', '$slicedOffset', ], [ - 'array(4 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 0 => \'quux\', \'quuz\' => \'corge\', 5 => \'grault\')', + 'array{4: \'foo\', 1: \'bar\', baz: \'qux\', 0: \'quux\', quuz: \'corge\', 5: \'grault\'}', '$slicedOffsetWithKeys', ], [ @@ -5297,24 +5176,20 @@ public function dataArrayFunctions(): array ]; } - /** - * @dataProvider dataArrayFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayFunctions')] public function testArrayFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-functions.php', $description, - $expression + $expression, ); } - public function dataFunctions(): array + public static function dataFunctions(): array { return [ [ @@ -5337,26 +5212,6 @@ public function dataFunctions(): array '(float|string)', '$microtimeBenevolent', ], - [ - 'int', - '$strtotimeNow', - ], - [ - 'false', - '$strtotimeInvalid', - ], - [ - 'int|false', - '$strtotimeUnknown', - ], - [ - '(int|false)', - '$strtotimeUnknown2', - ], - [ - 'int|false', - '$strtotimeCrash', - ], [ '-1', '$versionCompare1', @@ -5382,37 +5237,13 @@ public function dataFunctions(): array '$versionCompare6', ], [ - 'bool', + PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool', '$versionCompare7', ], [ - 'bool', + PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool', '$versionCompare8', ], - [ - 'int', - '$mbStrlenWithoutEncoding', - ], - [ - 'int', - '$mbStrlenWithValidEncoding', - ], - [ - 'int', - '$mbStrlenWithValidEncodingAlias', - ], - [ - 'false', - '$mbStrlenWithInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'int|false' : 'int', - '$mbStrlenWithValidAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'int|false' : 'int', - '$mbStrlenWithUnknownEncoding', - ], [ 'string', '$mbHttpOutputWithoutEncoding', @@ -5474,19 +5305,19 @@ public function dataFunctions(): array '$mbInternalEncodingWithUnknownEncoding', ], [ - 'array', + 'list', '$mbEncodingAliasesWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbEncodingAliasesWithInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithValidAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithUnknownEncoding', ], [ @@ -5498,7 +5329,7 @@ public function dataFunctions(): array '$mbChrWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbChrWithInvalidEncoding', ], [ @@ -5518,7 +5349,7 @@ public function dataFunctions(): array '$mbOrdWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbOrdWithInvalidEncoding', ], [ @@ -5530,11 +5361,11 @@ public function dataFunctions(): array '$mbOrdWithUnknownEncoding', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', '$gettimeofdayArrayWithoutArg', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', '$gettimeofdayArray', ], [ @@ -5542,64 +5373,28 @@ public function dataFunctions(): array '$gettimeofdayFloat', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}|float', '$gettimeofdayDefault', ], [ - '(array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float)', + '(array{sec: int, usec: int, minuteswest: int, dsttime: int}|float)', '$gettimeofdayBenevolent', ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$strSplitConstantStringWithoutDefinedParameters', - ], - [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$strSplitConstantStringWithoutDefinedSplitLength', - ], - [ - 'array&nonEmpty', - '$strSplitStringWithoutDefinedSplitLength', - ], - [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$strSplitConstantStringWithOneSplitLength', - ], - [ - "array('abcdef')", - '$strSplitConstantStringWithGreaterSplitLengthThanStringLength', - ], - [ - 'false', - '$strSplitConstantStringWithFailureSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$strSplitConstantStringWithInvalidSplitLengthType', - ], - [ - 'array&nonEmpty', - '$strSplitConstantStringWithVariableStringAndConstantSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$strSplitConstantStringWithVariableStringAndVariableSplitLength', - ], // parse_url [ 'array|int|string|false|null', '$parseUrlWithoutParameters', ], [ - "array('scheme' => 'http', 'host' => 'abc.def')", + 'array{scheme: \'http\', host: \'abc.def\'}', '$parseUrlConstantUrlWithoutComponent1', ], [ - "array('scheme' => 'http', 'host' => 'def.abc')", + 'array{scheme: \'http\', host: \'def.abc\'}', '$parseUrlConstantUrlWithoutComponent2', ], [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", + 'array{scheme?: 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', ], [ @@ -5619,15 +5414,15 @@ public function dataFunctions(): array '$parseUrlStringUrlWithComponentInvalid', ], [ - 'int|false|null', + 'int<0, 65535>|false|null', '$parseUrlStringUrlWithComponentPort', ], [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", + 'array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', '$parseUrlStringUrlWithoutComponent', ], [ - "array('path' => 'abc.def')", + 'array{path: \'abc.def\'}', "parse_url('abc.def')", ], [ @@ -5639,17 +5434,21 @@ public function dataFunctions(): array "parse_url('http://abc.def', PHP_URL_SCHEME)", ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$stat', ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$lstat', ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$fstat', ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}', + '$fileObjectStat', + ], [ 'string', '$base64DecodeWithoutStrict', @@ -5706,100 +5505,36 @@ public function dataFunctions(): array 'array|int|false', '$strWordCountStrTypeIndeterminant', ], - [ - 'string', - '$hashHmacMd5', - ], - [ - 'string', - '$hashHmacSha256', - ], - [ - 'false', - '$hashHmacNonCryptographic', - ], - [ - 'false', - '$hashHmacRandom', - ], - [ - 'string', - '$hashHmacVariable', - ], - [ - 'string|false', - '$hashHmacFileMd5', - ], - [ - 'string|false', - '$hashHmacFileSha256', - ], - [ - 'false', - '$hashHmacFileNonCryptographic', - ], - [ - 'false', - '$hashHmacFileRandom', - ], - [ - '(string|false)', - '$hashHmacFileVariable', - ], - [ - 'string', - '$hash', - ], - [ - 'string', - '$hashRaw', - ], - [ - 'false', - '$hashRandom', - ], - [ - 'string', - '$hashMixed', - ], ]; } - /** - * @dataProvider dataFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataFunctions')] public function testFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/functions.php', $description, - $expression + $expression, ); } - public function dataDioFunctions(): array + public static function dataDioFunctions(): array { return [ [ - 'array(\'device\' => int, \'inode\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'device_type\' => int, \'size\' => int, \'blocksize\' => int, \'blocks\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int)|null', + 'array{device: int, inode: int, mode: int, nlink: int, uid: int, gid: int, device_type: int, size: int, blocksize: int, blocks: int, atime: int, mtime: int, ctime: int}|null', '$stat', ], ]; } - /** - * @dataProvider dataDioFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataDioFunctions')] public function testDioFunctions( string $description, - string $expression + string $expression, ): void { if (!function_exists('dio_stat')) { @@ -5808,109 +5543,105 @@ public function testDioFunctions( $this->assertTypes( __DIR__ . '/data/dio-functions.php', $description, - $expression + $expression, ); } - public function dataSsh2Functions(): array + public static function dataSsh2Functions(): array { return [ [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$ssh2SftpStat', ], ]; } - /** - * @dataProvider dataSsh2Functions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSsh2Functions')] public function testSsh2Functions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/ssh2-functions.php', $description, - $expression + $expression, ); } - public function dataRangeFunction(): array + public static function dataRangeFunction(): array { return [ [ - 'array(2, 3, 4, 5)', + 'array{2, 3, 4, 5}', 'range(2, 5)', ], [ - 'array(2, 4)', + 'array{2, 4}', 'range(2, 5, 2)', ], [ - 'array(2.0, 3.0, 4.0, 5.0)', + 'array{2, 0}', + "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)', + 'array{2.1, 3.1, 4.1}', 'range(2.1, 5)', ], [ - 'array', + 'list', 'range(2, 5, $integer)', ], [ - 'array', + 'list', 'range($float, 5, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($float, $mixed, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($integer, $mixed)', ], [ - 'array(0 => 1, ?1 => 2)', + 'array{0: 1, 1?: 2}', 'range(1, doFoo() ? 1 : 2)', ], [ - 'array(0 => -1|1, ?1 => 0|2, ?2 => 1, ?3 => 2)', + 'array{0: -1, 1: 0, 2: 1, 3?: 2}|array{0: 1, 1?: 2}', 'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)', ], [ - 'array(3, 2, 1, 0, -1)', + 'array{3, 2, 1, 0, -1}', 'range(3, -1)', ], [ - 'array&nonEmpty', + 'non-empty-list>', 'range(0, 50)', ], ]; } - /** - * @dataProvider dataRangeFunction - * @param string $description - * @param string $expression - */ + #[DataProvider('dataRangeFunction')] public function testRangeFunction( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/range-function.php', $description, - $expression + $expression, ); } - public function dataSpecifiedTypesUsingIsFunctions(): array + public static function dataSpecifiedTypesUsingIsFunctions(): array { return [ [ @@ -5942,7 +5673,7 @@ public function dataSpecifiedTypesUsingIsFunctions(): array '$null', ], [ - 'array', + 'array', '$array', ], [ @@ -6052,24 +5783,20 @@ public function dataSpecifiedTypesUsingIsFunctions(): array ]; } - /** - * @dataProvider dataSpecifiedTypesUsingIsFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataSpecifiedTypesUsingIsFunctions')] public function testSpecifiedTypesUsingIsFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/specifiedTypesUsingIsFunctions.php', $description, - $expression + $expression, ); } - public function dataIterable(): array + public static function dataIterable(): array { return [ [ @@ -6141,7 +5868,7 @@ public function dataIterable(): array '$unionBar', ], [ - 'array&nonEmpty', + 'non-empty-array', '$mixedUnionIterableType', ], [ @@ -6219,24 +5946,20 @@ public function dataIterable(): array ]; } - /** - * @dataProvider dataIterable - * @param string $description - * @param string $expression - */ + #[DataProvider('dataIterable')] public function testIterable( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/iterable.php', $description, - $expression + $expression, ); } - public function dataArrayAccess(): array + public static function dataArrayAccess(): array { return [ [ @@ -6258,59 +5981,51 @@ public function dataArrayAccess(): array ]; } - /** - * @dataProvider dataArrayAccess - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayAccess')] public function testArrayAccess( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-accessable.php', $description, - $expression + $expression, ); } - public function dataVoid(): array + public static function dataVoid(): array { return [ [ - 'void', + 'null', '$this->doFoo()', ], [ - 'void', + 'null', '$this->doBar()', ], [ - 'void', + 'null', '$this->doConflictingVoid()', ], ]; } - /** - * @dataProvider dataVoid - * @param string $description - * @param string $expression - */ + #[DataProvider('dataVoid')] public function testVoid( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/void.php', $description, - $expression + $expression, ); } - public function dataNullableReturnTypes(): array + public static function dataNullableReturnTypes(): array { return [ [ @@ -6332,24 +6047,20 @@ public function dataNullableReturnTypes(): array ]; } - /** - * @dataProvider dataNullableReturnTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataNullableReturnTypes')] public function testNullableReturnTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/nullable-returnTypes.php', $description, - $expression + $expression, ); } - public function dataTernary(): array + public static function dataTernary(): array { return [ [ @@ -6379,24 +6090,20 @@ public function dataTernary(): array ]; } - /** - * @dataProvider dataTernary - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTernary')] public function testTernary( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/ternary.php', $description, - $expression + $expression, ); } - public function dataHeredoc(): array + public static function dataHeredoc(): array { return [ [ @@ -6410,24 +6117,20 @@ public function dataHeredoc(): array ]; } - /** - * @dataProvider dataHeredoc - * @param string $description - * @param string $expression - */ + #[DataProvider('dataHeredoc')] public function testHeredoc( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/heredoc.php', $description, - $expression + $expression, ); } - public function dataTypeElimination(): array + public static function dataTypeElimination(): array { return [ [ @@ -6583,27 +6286,22 @@ public function dataTypeElimination(): array ]; } - /** - * @dataProvider dataTypeElimination - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataTypeElimination')] public function testTypeElimination( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/type-elimination.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataMisleadingTypes(): array + public static function dataMisleadingTypes(): array { return [ [ @@ -6615,30 +6313,26 @@ public function dataMisleadingTypes(): array '$foo->misleadingIntReturnType()', ], [ - 'mixed', + PHP_VERSION_ID >= 80000 ? 'mixed' : 'MisleadingTypes\mixed', '$foo->misleadingMixedReturnType()', ], ]; } - /** - * @dataProvider dataMisleadingTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataMisleadingTypes')] public function testMisleadingTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/misleading-types.php', $description, - $expression + $expression, ); } - public function dataMisleadingTypesWithoutNamespace(): array + public static function dataMisleadingTypesWithoutNamespace(): array { return [ [ @@ -6652,24 +6346,20 @@ public function dataMisleadingTypesWithoutNamespace(): array ]; } - /** - * @dataProvider dataMisleadingTypesWithoutNamespace - * @param string $description - * @param string $expression - */ + #[DataProvider('dataMisleadingTypesWithoutNamespace')] public function testMisleadingTypesWithoutNamespace( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/misleading-types-without-namespace.php', $description, - $expression + $expression, ); } - public function dataUnresolvableTypes(): array + public static function dataUnresolvableTypes(): array { return [ [ @@ -6687,24 +6377,20 @@ public function dataUnresolvableTypes(): array ]; } - /** - * @dataProvider dataUnresolvableTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataUnresolvableTypes')] public function testUnresolvableTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/unresolvable-types.php', $description, - $expression + $expression, ); } - public function dataCombineTypes(): array + public static function dataCombineTypes(): array { return [ [ @@ -6718,24 +6404,20 @@ public function dataCombineTypes(): array ]; } - /** - * @dataProvider dataCombineTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCombineTypes')] public function testCombineTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/combine-types.php', $description, - $expression + $expression, ); } - public function dataConstants(): array + public static function dataConstants(): array { define('ConstantsForNodeScopeResolverTest\\FOO_CONSTANT', 1); @@ -6759,24 +6441,20 @@ public function dataConstants(): array ]; } - /** - * @dataProvider dataConstants - * @param string $description - * @param string $expression - */ + #[DataProvider('dataConstants')] public function testConstants( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/constants.php', $description, - $expression + $expression, ); } - public function dataFinally(): array + public static function dataFinally(): array { return [ [ @@ -6790,41 +6468,33 @@ public function dataFinally(): array ]; } - /** - * @dataProvider dataFinally - * @param string $description - * @param string $expression - */ + #[DataProvider('dataFinally')] public function testFinally( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/finally.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataFinally - * @param string $description - * @param string $expression - */ + #[DataProvider('dataFinally')] public function testFinallyWithEarlyTermination( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/finally-with-early-termination.php', $description, - $expression + $expression, ); } - public function dataInheritDocFromInterface(): array + public static function dataInheritDocFromInterface(): array { return [ [ @@ -6834,41 +6504,33 @@ public function dataInheritDocFromInterface(): array ]; } - /** - * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromInterface')] public function testInheritDocFromInterface( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-from-interface.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromInterface')] public function testInheritDocWithoutCurlyBracesFromInterface( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface.php', $description, - $expression + $expression, ); } - public function dataInheritDocFromInterface2(): array + public static function dataInheritDocFromInterface2(): array { return [ [ @@ -6878,43 +6540,35 @@ public function dataInheritDocFromInterface2(): array ]; } - /** - * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromInterface2')] public function testInheritDocFromInterface2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-from-interface2-definition.php'; $this->assertTypes( __DIR__ . '/data/inheritdoc-from-interface2.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromInterface2')] public function testInheritDocWithoutCurlyBracesFromInterface2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2-definition.php'; $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2.php', $description, - $expression + $expression, ); } - public function dataInheritDocFromTrait(): array + public static function dataInheritDocFromTrait(): array { return [ [ @@ -6924,41 +6578,33 @@ public function dataInheritDocFromTrait(): array ]; } - /** - * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromTrait')] public function testInheritDocFromTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-from-trait.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromTrait')] public function testInheritDocWithoutCurlyBracesFromTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait.php', $description, - $expression + $expression, ); } - public function dataInheritDocFromTrait2(): array + public static function dataInheritDocFromTrait2(): array { return [ [ @@ -6968,14 +6614,10 @@ public function dataInheritDocFromTrait2(): array ]; } - /** - * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromTrait2')] public function testInheritDocFromTrait2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-from-trait2-definition.php'; @@ -6983,18 +6625,14 @@ public function testInheritDocFromTrait2( $this->assertTypes( __DIR__ . '/data/inheritdoc-from-trait2.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInheritDocFromTrait2')] public function testInheritDocWithoutCurlyBracesFromTrait2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition.php'; @@ -7002,11 +6640,11 @@ public function testInheritDocWithoutCurlyBracesFromTrait2( $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2.php', $description, - $expression + $expression, ); } - public function dataResolveStatic(): array + public static function dataResolveStatic(): array { return [ [ @@ -7018,7 +6656,7 @@ public function dataResolveStatic(): array '\ResolveStatic\Bar::create()', ], [ - 'array(\'foo\' => ResolveStatic\Bar)', + 'array{foo: ResolveStatic\\Bar}', '$bar->returnConstantArray()', ], [ @@ -7032,24 +6670,20 @@ public function dataResolveStatic(): array ]; } - /** - * @dataProvider dataResolveStatic - * @param string $description - * @param string $expression - */ + #[DataProvider('dataResolveStatic')] public function testResolveStatic( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/resolve-static.php', $description, - $expression + $expression, ); } - public function dataLoopVariables(): array + public static function dataLoopVariables(): array { return [ [ @@ -7068,12 +6702,7 @@ public function dataLoopVariables(): array "'end'", ], [ - 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', - '$foo', - "'afterLoop'", - ], - [ - 'int|null', + 'int<1, max>|null', '$nullableVal', "'begin'", ], @@ -7083,15 +6712,10 @@ public function dataLoopVariables(): array "'nullableValIf'", ], [ - 'int', + 'int<10, max>', '$nullableVal', "'nullableValElse'", ], - [ - 'int|null', - '$nullableVal', - "'afterLoop'", - ], [ 'LoopVariables\Foo|false', '$falseOrObject', @@ -7102,15 +6726,10 @@ public function dataLoopVariables(): array '$falseOrObject', "'end'", ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'afterLoop'", - ], ]; } - public function dataForeachLoopVariables(): array + public static function dataForeachLoopVariables(): array { return [ [ @@ -7149,12 +6768,12 @@ public function dataForeachLoopVariables(): array "'end'", ], [ - 'array&nonEmpty', + 'non-empty-list<1|2|3>', '$integers', "'end'", ], [ - 'array', + 'list<1|2|3>', '$integers', "'afterLoop'", ], @@ -7164,7 +6783,7 @@ public function dataForeachLoopVariables(): array "'begin'", ], [ - 'array&nonEmpty', + 'non-empty-array', '$this->property', "'end'", ], @@ -7174,132 +6793,159 @@ public function dataForeachLoopVariables(): array "'afterLoop'", ], [ - 'int', + 'int<0, max>', '$i', "'begin'", ], [ - 'int', + 'int<0, max>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], ]; } - public function dataWhileLoopVariables(): array + public static function dataWhileLoopVariables(): array { return [ [ - 'int', + 'int<1, 10>', '$i', "'begin'", ], [ - 'int', + 'int<1, 10>', '$i', "'end'", ], [ - 'int', + 'int<0, 10>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], ]; } - - public function dataForLoopVariables(): array + public static function dataForLoopVariables(): array { return [ [ - 'int', + 'int<0, 9>', '$i', "'begin'", ], [ - 'int', + 'int<0, 9>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo', + '$falseOrObject', + "'afterLoop'", + ], ]; } - - - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForeachLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataForeachLoopVariables')] public function testForeachLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/foreach-loop-variables.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - /** - * @dataProvider dataLoopVariables - * @dataProvider dataWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataWhileLoopVariables')] public function testWhileLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/while-loop-variables.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataForLoopVariables')] public function testForLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/for-loop-variables.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataDoWhileLoopVariables(): array + public static function dataDoWhileLoopVariables(): array { return [ [ @@ -7323,22 +6969,22 @@ public function dataDoWhileLoopVariables(): array "'afterLoop'", ], [ - 'int', + 'int<0, max>', '$i', "'begin'", ], [ - 'int', + 'int<1, max>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], [ - 'int|null', + 'int<1, max>|null', '$nullableVal', "'begin'", ], @@ -7348,12 +6994,12 @@ public function dataDoWhileLoopVariables(): array "'nullableValIf'", ], [ - 'int', + 'int<10, max>', '$nullableVal', "'nullableValElse'", ], [ - 'int', + '1|int<10, max>', '$nullableVal', "'afterLoop'", ], @@ -7391,27 +7037,22 @@ public function dataDoWhileLoopVariables(): array ]; } - /** - * @dataProvider dataDoWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataDoWhileLoopVariables')] public function testDoWhileLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/do-while-loop-variables.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataMultipleClassesInOneFile(): array + public static function dataMultipleClassesInOneFile(): array { return [ [ @@ -7427,27 +7068,22 @@ public function dataMultipleClassesInOneFile(): array ]; } - /** - * @dataProvider dataMultipleClassesInOneFile - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataMultipleClassesInOneFile')] public function testMultipleClassesInOneFile( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/multiple-classes-per-file.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataCallingMultipleClassesInOneFile(): array + public static function dataCallingMultipleClassesInOneFile(): array { return [ [ @@ -7461,28 +7097,24 @@ public function dataCallingMultipleClassesInOneFile(): array ]; } - /** - * @dataProvider dataCallingMultipleClassesInOneFile - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCallingMultipleClassesInOneFile')] public function testCallingMultipleClassesInOneFile( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/calling-multiple-classes-per-file.php', $description, - $expression + $expression, ); } - public function dataExplode(): array + public static function dataExplode(): array { return [ [ - 'array&nonEmpty', + 'non-empty-list', '$sureArray', ], [ @@ -7490,38 +7122,34 @@ public function dataExplode(): array '$sureFalse', ], [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$arrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$anotherArrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '((array&nonEmpty)|false)' : 'array&nonEmpty', + PHP_VERSION_ID < 80000 ? '(non-empty-list|false)' : 'non-empty-list', '$benevolentArrayOrFalse', ], ]; } - /** - * @dataProvider dataExplode - * @param string $description - * @param string $expression - */ + #[DataProvider('dataExplode')] public function testExplode( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/explode.php', $description, - $expression + $expression, ); } - public function dataArrayPointerFunctions(): array + public static function dataArrayPointerFunctions(): array { return [ [ @@ -7548,6 +7176,18 @@ public function dataArrayPointerFunctions(): array '\'baz\'|\'foo\'', 'reset($conditionalArray)', ], + [ + '0|1', + 'reset($constantArrayOptionalKeys1)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys2)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys3)', + ], [ 'mixed', 'end()', @@ -7572,31 +7212,39 @@ public function dataArrayPointerFunctions(): array '\'bar\'|\'baz\'', 'end($secondConditionalArray)', ], + [ + '2', + 'end($constantArrayOptionalKeys1)', + ], + [ + '2', + 'end($constantArrayOptionalKeys2)', + ], + [ + '1|2', + 'end($constantArrayOptionalKeys3)', + ], ]; } - /** - * @dataProvider dataArrayPointerFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayPointerFunctions')] public function testArrayPointerFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-pointer-functions.php', $description, - $expression + $expression, ); } - public function dataReplaceFunctions(): array + public static function dataReplaceFunctions(): array { return [ [ - 'string', + 'lowercase-string&non-falsy-string', '$expectedString', ], [ @@ -7604,39 +7252,39 @@ public function dataReplaceFunctions(): array '$expectedString2', ], [ - 'string|null', + '(lowercase-string&non-falsy-string)|null', '$anotherExpectedString', ], [ - 'array(\'a\' => string, \'b\' => string)', + 'array{a: lowercase-string&non-falsy-string, b: lowercase-string&non-falsy-string}', '$expectedArray', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a?: string, b?: string}', '$expectedArray2', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a?: lowercase-string&non-falsy-string, b?: lowercase-string&non-falsy-string}', '$anotherExpectedArray', ], [ - 'array|string', + 'array{}|(lowercase-string&non-falsy-string)', '$expectedArrayOrString', ], [ - '(array|string)', + '(array|string)', '$expectedBenevolentArrayOrString', ], [ - 'array|string|null', + 'array{}|string|null', '$expectedArrayOrString2', ], [ - 'array|string|null', + 'array{}|(lowercase-string&non-falsy-string)|null', '$anotherExpectedArrayOrString', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a?: string, b?: string}', 'preg_replace_callback_array($callbacks, $array)', ], [ @@ -7654,24 +7302,23 @@ public function dataReplaceFunctions(): array ]; } - /** - * @dataProvider dataReplaceFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataReplaceFunctions')] public function testReplaceFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/replaceFunctions.php', $description, - $expression + $expression, ); } - public function dataFilterVar(): Generator + /** + * @return Generator + */ + public static function dataFilterVar(): Generator { $typesAndFilters = [ 'string' => [ @@ -7684,11 +7331,13 @@ public function dataFilterVar(): Generator 'FILTER_SANITIZE_SPECIAL_CHARS', 'FILTER_SANITIZE_STRING', 'FILTER_SANITIZE_URL', + 'FILTER_VALIDATE_REGEXP', + ], + 'non-falsy-string' => [ 'FILTER_VALIDATE_EMAIL', 'FILTER_VALIDATE_IP', '$filterIp', 'FILTER_VALIDATE_MAC', - 'FILTER_VALIDATE_REGEXP', 'FILTER_VALIDATE_URL', ], 'int' => ['FILTER_VALIDATE_INT'], @@ -7776,7 +7425,7 @@ public function dataFilterVar(): Generator ]; } - public function dataFilterVarUnchanged(): array + public static function dataFilterVarUnchanged(): array { return [ [ @@ -7814,25 +7463,21 @@ public function dataFilterVarUnchanged(): array ]; } - /** - * @dataProvider dataFilterVar - * @dataProvider dataFilterVarUnchanged - * @param string $description - * @param string $expression - */ + #[DataProvider('dataFilterVar')] + #[DataProvider('dataFilterVarUnchanged')] public function testFilterVar( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/filterVar.php', $description, - $expression + $expression, ); } - public function dataClosureWithUsePassedByReference(): array + public static function dataClosureWithUsePassedByReference(): array { return [ [ @@ -7891,17 +7536,17 @@ public function dataClosureWithUsePassedByReference(): array "'beforeCallback'", ], [ - 'int', + 'int<1, max>', '$incrementedInside', "'inCallbackBeforeAssign'", ], [ - 'int', + 'int<2, max>', '$incrementedInside', "'inCallbackAfterAssign'", ], [ - 'int', + 'int<1, max>', '$incrementedInside', "'afterCallback'", ], @@ -7928,27 +7573,22 @@ public function dataClosureWithUsePassedByReference(): array ]; } - /** - * @dataProvider dataClosureWithUsePassedByReference - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataClosureWithUsePassedByReference')] public function testClosureWithUsePassedByReference( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataClosureWithUsePassedByReferenceInMethodCall(): array + public static function dataClosureWithUsePassedByReferenceInMethodCall(): array { return [ [ @@ -7958,24 +7598,20 @@ public function dataClosureWithUsePassedByReferenceInMethodCall(): array ]; } - /** - * @dataProvider dataClosureWithUsePassedByReferenceInMethodCall - * @param string $description - * @param string $expression - */ + #[DataProvider('dataClosureWithUsePassedByReferenceInMethodCall')] public function testClosureWithUsePassedByReferenceInMethodCall( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference-in-call.php', $description, - $expression + $expression, ); } - public function dataClosureWithUsePassedByReferenceReturn(): array + public static function dataClosureWithUsePassedByReferenceReturn(): array { return [ [ @@ -8001,7 +7637,7 @@ public function dataClosureWithUsePassedByReferenceReturn(): array ]; } - public function dataStaticClosure(): array + public static function dataStaticClosure(): array { return [ [ @@ -8011,44 +7647,35 @@ public function dataStaticClosure(): array ]; } - /** - * @dataProvider dataStaticClosure - * @param string $description - * @param string $expression - */ + #[DataProvider('dataStaticClosure')] public function testStaticClosure( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/static-closure.php', $description, - $expression + $expression, ); } - /** - * @dataProvider dataClosureWithUsePassedByReferenceReturn - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataClosureWithUsePassedByReferenceReturn')] public function testClosureWithUsePassedByReferenceReturn( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference-return.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataClosureWithInferredTypehint(): array + public static function dataClosureWithInferredTypehint(): array { return [ [ @@ -8062,14 +7689,10 @@ public function dataClosureWithInferredTypehint(): array ]; } - /** - * @dataProvider dataClosureWithInferredTypehint - * @param string $description - * @param string $expression - */ + #[DataProvider('dataClosureWithInferredTypehint')] public function testClosureWithInferredTypehint( string $description, - string $expression + string $expression, ): void { $this->assertTypes( @@ -8078,11 +7701,11 @@ public function testClosureWithInferredTypehint( $expression, 'die', [], - false + false, ); } - public function dataTraitsPhpDocs(): array + public static function dataTraitsPhpDocs(): array { return [ [ @@ -8172,59 +7795,51 @@ public function dataTraitsPhpDocs(): array ]; } - /** - * @dataProvider dataTraitsPhpDocs - * @param string $description - * @param string $expression - */ + #[DataProvider('dataTraitsPhpDocs')] public function testTraitsPhpDocs( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/traits/traits.php', $description, - $expression + $expression, ); } - public function dataPassedByReference(): array + public static function dataPassedByReference(): array { return [ [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '$arr', ], [ - 'mixed', + 'array', '$matches', ], [ - 'mixed', + 'string', '$s', ], ]; } - /** - * @dataProvider dataPassedByReference - * @param string $description - * @param string $expression - */ + #[DataProvider('dataPassedByReference')] public function testPassedByReference( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/passed-by-reference.php', $description, - $expression + $expression, ); } - public function dataCallables(): array + public static function dataCallables(): array { return [ [ @@ -8236,11 +7851,11 @@ public function dataCallables(): array '$closure()', ], [ - 'Callables\\Bar', + PHP_VERSION_ID < 80000 ? 'Callables\\Bar' : '*ERROR*', '$arrayWithStaticMethod()', ], [ - 'float', + PHP_VERSION_ID < 80000 ? 'float' : '*ERROR*', '$stringWithStaticMethod()', ], [ @@ -8254,48 +7869,44 @@ public function dataCallables(): array ]; } - /** - * @dataProvider dataCallables - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCallables')] public function testCallables( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/callables.php', $description, - $expression + $expression, ); } - public function dataArrayKeysInBranches(): array + public static function dataArrayKeysInBranches(): array { return [ [ - 'array(\'i\' => int, \'j\' => int, \'k\' => int, \'key\' => DateTimeImmutable, \'l\' => 1, \'m\' => 5, ?\'n\' => \'str\')', + 'array{i: int<1, max>, j: int, k: int<1, max>, l: 1, m: 5, key: DateTimeImmutable, n?: \'str\'}', '$array', ], [ - 'array', + 'non-empty-array&hasOffsetValue(\'key\', mixed~null)', '$generalArray', ], [ - 'mixed', // should be DateTimeImmutable + 'mixed~null', '$generalArray[\'key\']', ], [ - 'array(0 => \'foo\', 1 => \'bar\', ?2 => \'baz\')', + 'array{0: \'foo\', 1: \'bar\', 2?: \'baz\'}', '$arrayAppendedInIf', ], [ - 'array', + 'non-empty-list<\'bar\'|\'baz\'|\'foo\'>', '$arrayAppendedInForeach', ], [ - 'array', + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ @@ -8303,7 +7914,7 @@ public function dataArrayKeysInBranches(): array '$array[\'n\']', ], [ - 'int', + 'int<0, max>', '$incremented', ], [ @@ -8313,24 +7924,20 @@ public function dataArrayKeysInBranches(): array ]; } - /** - * @dataProvider dataArrayKeysInBranches - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayKeysInBranches')] public function testArrayKeysInBranches( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-keys-branches.php', $description, - $expression + $expression, ); } - public function dataSpecifiedFunctionCall(): array + public static function dataSpecifiedFunctionCall(): array { return [ [ @@ -8361,27 +7968,22 @@ public function dataSpecifiedFunctionCall(): array ]; } - /** - * @dataProvider dataSpecifiedFunctionCall - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataSpecifiedFunctionCall')] public function testSpecifiedFunctionCall( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/specified-function-call.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataElementsOnMixed(): array + public static function dataElementsOnMixed(): array { return [ [ @@ -8407,24 +8009,20 @@ public function dataElementsOnMixed(): array ]; } - /** - * @dataProvider dataElementsOnMixed - * @param string $description - * @param string $expression - */ + #[DataProvider('dataElementsOnMixed')] public function testElementsOnMixed( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/mixed-elements.php', $description, - $expression + $expression, ); } - public function dataCaseInsensitivePhpDocTypes(): array + public static function dataCaseInsensitivePhpDocTypes(): array { return [ [ @@ -8438,24 +8036,20 @@ public function dataCaseInsensitivePhpDocTypes(): array ]; } - /** - * @dataProvider dataCaseInsensitivePhpDocTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCaseInsensitivePhpDocTypes')] public function testCaseInsensitivePhpDocTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/case-insensitive-phpdocs.php', $description, - $expression + $expression, ); } - public function dataConstantTypeAfterDuplicateCondition(): array + public static function dataConstantTypeAfterDuplicateCondition(): array { return [ [ @@ -8521,27 +8115,22 @@ public function dataConstantTypeAfterDuplicateCondition(): array ]; } - /** - * @dataProvider dataConstantTypeAfterDuplicateCondition - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataConstantTypeAfterDuplicateCondition')] public function testConstantTypeAfterDuplicateCondition( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/constant-types-duplicate-condition.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataAnonymousClass(): array + public static function dataAnonymousClass(): array { return [ [ @@ -8577,27 +8166,22 @@ public function dataAnonymousClass(): array ]; } - /** - * @dataProvider dataAnonymousClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataAnonymousClass')] public function testAnonymousClassName( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-class-name.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataAnonymousClassInTrait(): array + public static function dataAnonymousClassInTrait(): array { return [ [ @@ -8607,30 +8191,66 @@ public function dataAnonymousClassInTrait(): array ]; } - /** - * @dataProvider dataAnonymousClassInTrait - * @param string $description - * @param string $expression - */ + #[DataProvider('dataAnonymousClassInTrait')] public function testAnonymousClassNameInTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-class-name-in-trait.php', $description, - $expression + $expression, + ); + } + + public static 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 + public static function dataDynamicConstants(): array { return [ [ 'string', 'DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', ], + [ + 'string|null', + 'DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES_IN_CLASS', + ], [ "'abc123def'", 'DynamicConstants\DynamicConstantClass::PURE_CONSTANT_IN_CLASS', @@ -8647,17 +8267,17 @@ public function dataDynamicConstants(): array '123', 'GLOBAL_PURE_CONSTANT', ], + [ + 'string|null', + 'GLOBAL_DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES', + ], ]; } - /** - * @dataProvider dataDynamicConstants - * @param string $description - * @param string $expression - */ + #[DataProvider('dataDynamicConstants')] public function testDynamicConstants( string $description, - string $expression + string $expression, ): void { $this->assertTypes( @@ -8666,13 +8286,56 @@ public function testDynamicConstants( $expression, 'die', [ - 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', - 'GLOBAL_DYNAMIC_CONSTANT', - ] + 0 => 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', + 1 => 'GLOBAL_DYNAMIC_CONSTANT', + 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES_IN_CLASS' => 'string|null', + 'GLOBAL_DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES' => 'string|null', + ], ); } - public function dataIsset(): array + public static function dataDynamicConstantsWithNativeTypes(): array + { + return [ + [ + 'int', + 'DynamicConstantNativeTypes\Foo::FOO', + ], + [ + 'int|string', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + [ + 'int', + '$foo::FOO', + ], + [ + 'int|string', + '$foo::BAR', + ], + ]; + } + + #[RequiresPhp('>= 8.3')] + #[DataProvider('dataDynamicConstantsWithNativeTypes')] + public function testDynamicConstantsWithNativeTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/dynamic-constant-native-types.php', + $description, + $expression, + 'die', + [ + 'DynamicConstantNativeTypes\Foo::FOO', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + ); + } + + public static function dataIsset(): array { return [ [ @@ -8680,19 +8343,19 @@ public function dataIsset(): array '$array[\'b\']', ], [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3, ?\'c\' => 4)', + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}', '$array', ], [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3|null, ?\'c\' => 4)', + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', '$arrayCopy', ], [ - 'array(\'a\' => 1|2|3, ?\'c\' => 4)', + 'array{a: 2}', '$anotherArrayCopy', ], [ - 'array', + 'array{a: 1, b: 2}|array{a: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', '$yetAnotherArrayCopy', ], [ @@ -8700,11 +8363,11 @@ public function dataIsset(): array '$mixedIsset', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', '$mixedArrayKeyExists', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffsetValue(\'a\', int)', '$integers', ], [ @@ -8724,7 +8387,7 @@ public function dataIsset(): array '$lookup[$a] ?? false', ], [ - '\'foo\'|false', + '\'foo\'', '$nullableArray[\'a\'] ?? false', ], [ @@ -8732,30 +8395,26 @@ public function dataIsset(): array '$nullableArray[\'b\'] ?? false', ], [ - '\'baz\'|false', + '\'baz\'', '$nullableArray[\'c\'] ?? false', ], ]; } - /** - * @dataProvider dataIsset - * @param string $description - * @param string $expression - */ + #[DataProvider('dataIsset')] public function testIsset( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/isset.php', $description, - $expression + $expression, ); } - public function dataPropertyArrayAssignment(): array + public static function dataPropertyArrayAssignment(): array { return [ [ @@ -8764,7 +8423,7 @@ public function dataPropertyArrayAssignment(): array "'start'", ], [ - 'array()', + 'array{}', '$this->property', "'emptyArray'", ], @@ -8774,7 +8433,7 @@ public function dataPropertyArrayAssignment(): array "'emptyArray'", ], [ - 'array(\'foo\' => 1)', + 'array{foo: 1}', '$this->property', "'afterAssignment'", ], @@ -8786,66 +8445,22 @@ public function dataPropertyArrayAssignment(): array ]; } - /** - * @dataProvider dataPropertyArrayAssignment - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataPropertyArrayAssignment')] public function testPropertyArrayAssignment( string $description, string $expression, - string $evaluatedPointExpression + 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 + $evaluatedPointExpression, ); } - public function dataGetParentClass(): array + public static function dataGetParentClass(): array { return [ [ @@ -8919,27 +8534,22 @@ public function dataGetParentClass(): array ]; } - /** - * @dataProvider dataGetParentClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataGetParentClass')] public function testGetParentClass( string $description, string $expression, - string $evaluatedPointExpression = 'die' + string $evaluatedPointExpression = 'die', ): void { $this->assertTypes( __DIR__ . '/data/get-parent-class.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataIsCountable(): array + public static function dataIsCountable(): array { return [ [ @@ -8955,43 +8565,38 @@ public function dataIsCountable(): array ]; } - /** - * @dataProvider dataIsCountable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataIsCountable')] public function testIsCountable( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/is_countable.php', $description, $expression, - $evaluatedPointExpression + $evaluatedPointExpression, ); } - public function dataPhp73Functions(): array + public static function dataPhp73Functions(): array { return [ [ - 'string|false', + 'non-empty-string|false', 'json_encode($mixed)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ @@ -8999,11 +8604,11 @@ public function dataPhp73Functions(): array 'json_decode($mixed)', ], [ - 'mixed~false', + 'mixed', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'mixed~false', + 'mixed', 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ @@ -9055,179 +8660,62 @@ public function dataPhp73Functions(): array 'array_key_last($anotherLiteralArray)', ], [ - 'array(int, int)', - '$hrtime1', + "'a'|'b'", + 'array_key_first($constantArrayOptionalKeys1)', ], [ - 'array(int, int)', - '$hrtime2', + "'c'", + 'array_key_last($constantArrayOptionalKeys1)', ], [ - '(float|int)', - '$hrtime3', + "'a'", + 'array_key_first($constantArrayOptionalKeys2)', ], [ - '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 dataPhp74Functions(): array - { - return [ - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithoutDefinedParameters', + "'c'", + 'array_key_last($constantArrayOptionalKeys2)', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$mbStrSplitConstantStringWithoutDefinedSplitLength', + "'a'", + 'array_key_first($constantArrayOptionalKeys3)', ], [ - 'array&nonEmpty', - '$mbStrSplitStringWithoutDefinedSplitLength', + "'b'|'c'", + 'array_key_last($constantArrayOptionalKeys3)', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$mbStrSplitConstantStringWithOneSplitLength', - ], - [ - "array('abcdef')", - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithInvalidSplitLengthType', - ], - [ - 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', - ], - [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', - ], - [ - "array('abcdef')", - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', - ], - [ - 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', + 'array{int, int}', + '$hrtime1', ], [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', + 'array{int, int}', + '$hrtime2', ], [ - 'false', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', + '(float|int)', + '$hrtime3', ], [ - PHP_VERSION_ID < 80000 ? '(array&nonEmpty)|false' : 'array&nonEmpty', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', + 'array{int, int}|float|int', + '$hrtime4', ], ]; } - /** - * @dataProvider dataPhp74Functions - * @param string $description - * @param string $expression - */ - public function testPhp74Functions( + #[DataProvider('dataPhp73Functions')] + public function testPhp73Functions( string $description, - string $expression + string $expression, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4'); - } $this->assertTypes( - __DIR__ . '/data/php74_functions.php', + __DIR__ . '/data/php73_functions.php', $description, - $expression + $expression, ); } - public function dataUnionMethods(): array + public static function dataUnionMethods(): array { return [ [ @@ -9241,55 +8729,43 @@ public function dataUnionMethods(): array ]; } - /** - * @dataProvider dataUnionMethods - * @param string $description - * @param string $expression - */ + #[DataProvider('dataUnionMethods')] public function testUnionMethods( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-methods.php', $description, - $expression + $expression, ); } - public function dataUnionProperties(): array + public static function dataUnionProperties(): array { return [ [ 'UnionProperties\Bar|UnionProperties\Foo', '$something->doSomething', ], - [ - 'UnionProperties\Bar|UnionProperties\Foo', - '$something::$doSomething', - ], ]; } - /** - * @dataProvider dataUnionProperties - * @param string $description - * @param string $expression - */ + #[DataProvider('dataUnionProperties')] public function testUnionProperties( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-properties.php', $description, - $expression + $expression, ); } - public function dataAssignmentInCondition(): array + public static function dataAssignmentInCondition(): array { return [ [ @@ -9299,113 +8775,97 @@ public function dataAssignmentInCondition(): array ]; } - /** - * @dataProvider dataAssignmentInCondition - * @param string $description - * @param string $expression - */ + #[DataProvider('dataAssignmentInCondition')] public function testAssignmentInCondition( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/assignment-in-condition.php', $description, - $expression + $expression, ); } - public function dataGeneralizeScope(): array + public static function dataGeneralizeScope(): array { return [ [ - "array int, 'loadCount' => int, 'removeCount' => int, 'saveCount' => int)>>", + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; } - /** - * @dataProvider dataGeneralizeScope - * @param string $description - * @param string $expression - */ + #[DataProvider('dataGeneralizeScope')] public function testGeneralizeScope( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/generalize-scope.php', $description, - $expression + $expression, ); } - public function dataGeneralizeScopeRecursiveType(): array + public static function dataGeneralizeScopeRecursiveType(): array { return [ [ - 'array()|array(\'foo\' => array)', + 'array{}|array{foo?: array}', '$data', ], ]; } - /** - * @dataProvider dataGeneralizeScopeRecursiveType - * @param string $description - * @param string $expression - */ + #[DataProvider('dataGeneralizeScopeRecursiveType')] public function testGeneralizeScopeRecursiveType( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/generalize-scope-recursive.php', $description, - $expression + $expression, ); } - public function dataArrayShapesInPhpDoc(): array + public static function dataArrayShapesInPhpDoc(): array { return [ [ - 'array(0 => string, 1 => ArrayShapesInPhpDoc\Foo, \'foo\' => ArrayShapesInPhpDoc\Bar, 2 => ArrayShapesInPhpDoc\Baz)', + 'array{0: string, 1: ArrayShapesInPhpDoc\\Foo, foo: ArrayShapesInPhpDoc\\Bar, 2: ArrayShapesInPhpDoc\\Baz}', '$one', ], [ - 'array(0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', + 'array{0: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', '$two', ], [ - 'array(?0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', + 'array{0?: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', '$three', ], ]; } - /** - * @dataProvider dataArrayShapesInPhpDoc - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrayShapesInPhpDoc')] public function testArrayShapesInPhpDoc( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-shapes.php', $description, - $expression + $expression, ); } - public function dataInferPrivatePropertyTypeFromConstructor(): array + public static function dataInferPrivatePropertyTypeFromConstructor(): array { return [ [ @@ -9437,30 +8897,26 @@ public function dataInferPrivatePropertyTypeFromConstructor(): array '$this->bool', ], [ - 'array', + 'array', '$this->array', ], ]; } - /** - * @dataProvider dataInferPrivatePropertyTypeFromConstructor - * @param string $description - * @param string $expression - */ + #[DataProvider('dataInferPrivatePropertyTypeFromConstructor')] public function testInferPrivatePropertyTypeFromConstructor( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/infer-private-property-type-from-constructor.php', $description, - $expression + $expression, ); } - public function dataPropertyNativeTypes(): array + public static function dataPropertyNativeTypes(): array { return [ [ @@ -9478,27 +8934,20 @@ public function dataPropertyNativeTypes(): array ]; } - /** - * @dataProvider dataPropertyNativeTypes - * @param string $description - * @param string $expression - */ + #[DataProvider('dataPropertyNativeTypes')] public function testPropertyNativeTypes( string $description, - string $expression + string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/property-native-types.php', $description, - $expression + $expression, ); } - public function dataArrowFunctions(): array + public static function dataArrowFunctions(): array { return [ [ @@ -9510,33 +8959,26 @@ public function dataArrowFunctions(): array '$x()', ], [ - 'array(\'a\' => 1, \'b\' => 2)', + 'array{a: 1, b: 2}', '$y()', ], ]; } - /** - * @dataProvider dataArrowFunctions - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrowFunctions')] public function testArrowFunctions( string $description, - string $expression + string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/arrow-functions.php', $description, - $expression + $expression, ); } - public function dataArrowFunctionsInside(): array + public static function dataArrowFunctionsInside(): array { return [ [ @@ -9554,27 +8996,20 @@ public function dataArrowFunctionsInside(): array ]; } - /** - * @dataProvider dataArrowFunctionsInside - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArrowFunctionsInside')] public function testArrowFunctionsInside( string $description, - string $expression + string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/arrow-functions-inside.php', $description, - $expression + $expression, ); } - public function dataCoalesceAssign(): array + public static function dataCoalesceAssign(): array { return [ [ @@ -9598,11 +9033,11 @@ public function dataCoalesceAssign(): array '$arrayWithMaybeFoo[\'foo\'] ??= \'bar\'', ], [ - 'array(\'foo\' => \'foo\')', + 'array{foo: \'foo\'}', '$arrayAfterAssignment', ], [ - 'array(\'foo\' => \'foo\')', + 'array{foo: \'foo\'}', '$arrayWithFooAfterAssignment', ], [ @@ -9616,141 +9051,90 @@ public function dataCoalesceAssign(): array ]; } - /** - * @dataProvider dataCoalesceAssign - * @param string $description - * @param string $expression - */ + #[DataProvider('dataCoalesceAssign')] public function testCoalesceAssign( string $description, - string $expression + string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/coalesce-assign.php', $description, - $expression + $expression, ); } - public function dataArraySpread(): array + public static function dataArraySpread(): array { return [ [ - 'array&nonEmpty', + 'non-empty-list', '$integersOne', ], [ - 'array&nonEmpty', + 'non-empty-list', '$integersTwo', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersThree', ], [ - 'array&nonEmpty', + 'non-empty-list', '$integersFour', ], [ - 'array&nonEmpty', + 'non-empty-list', '$integersFive', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersSix', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersSeven', ], ]; } - /** - * @dataProvider dataArraySpread - * @param string $description - * @param string $expression - */ + #[DataProvider('dataArraySpread')] public function testArraySpread( string $description, - string $expression + string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/array-spread.php', $description, - $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 + $expression, ); } - public function dataPhp74FunctionsIn74(): array + public static function dataPhp74FunctionsIn74(): array { return [ [ - 'array', + 'list', 'password_algos()', ], ]; } - /** - * @dataProvider dataPhp74FunctionsIn74 - * @param string $description - * @param string $expression - */ + #[DataProvider('dataPhp74FunctionsIn74')] public function testPhp74FunctionsIn74( string $description, - string $expression + string $expression, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/die-74.php', $description, - $expression + $expression, ); } - public function dataTryCatchScope(): array + public static function dataTryCatchScope(): array { return [ [ @@ -9771,16 +9155,11 @@ public function dataTryCatchScope(): array ]; } - /** - * @dataProvider dataTryCatchScope - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ + #[DataProvider('dataTryCatchScope')] public function testTryCatchScope( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( @@ -9789,17 +9168,12 @@ public function testTryCatchScope( $expression, $evaluatedPointExpression, [], - false + false, ); } /** - * @param string $file - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression * @param string[] $dynamicConstantNames - * @param bool $useCache */ private function assertTypes( string $file, @@ -9807,41 +9181,42 @@ private function assertTypes( string $expression, string $evaluatedPointExpression = 'die', array $dynamicConstantNames = [], - bool $useCache = true + bool $useCache = true, ): void { $assertType = function (Scope $scope) use ($expression, $description, $evaluatedPointExpression): void { - /** @var \PhpParser\Node\Stmt\Expression $expressionNode */ + /** @var Node\Stmt\Expression $expressionNode */ $expressionNode = $this->getParser()->parseString(sprintf('getType($expressionNode->expr); $this->assertTypeDescribe( $description, $type, - sprintf('%s at %s', $expression, $evaluatedPointExpression) + sprintf('%s at %s', $expression, $evaluatedPointExpression), ); }; if ($useCache && isset(self::$assertTypesCache[$file][$evaluatedPointExpression])) { $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; - } - - self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; - - $assertType($scope); - }, - $dynamicConstantNames - ); + + self::processFile( + $file, + static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { + if ($node instanceof VirtualNode) { + return; + } + $printer = new Printer(); + $printedNode = $printer->prettyPrint([$node]); + if ($printedNode !== $evaluatedPointExpression) { + return; + } + + self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; + + $assertType($scope); + }, + $dynamicConstantNames, + ); } public static function getAdditionalConfigFiles(): array @@ -9852,7 +9227,7 @@ public static function getAdditionalConfigFiles(): array ]; } - public function dataDeclareStrictTypes(): array + public static function dataDeclareStrictTypes(): array { return [ [ @@ -9870,14 +9245,10 @@ public function dataDeclareStrictTypes(): array ]; } - /** - * @dataProvider dataDeclareStrictTypes - * @param string $file - * @param bool $result - */ + #[DataProvider('dataDeclareStrictTypes')] public function testDeclareStrictTypes(string $file, bool $result): void { - $this->processFile($file, function (\PhpParser\Node $node, Scope $scope) use ($result): void { + self::processFile($file, function (Node $node, Scope $scope) use ($result): void { if (!($node instanceof Exit_)) { return; } @@ -9888,7 +9259,7 @@ public function testDeclareStrictTypes(string $file, bool $result): void public function testEarlyTermination(): void { - $this->processFile(__DIR__ . '/data/early-termination.php', function (\PhpParser\Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -9899,7 +9270,7 @@ public function testEarlyTermination(): void }); } - protected function getEarlyTerminatingMethodCalls(): array + protected static function getEarlyTerminatingMethodCalls(): array { return [ \EarlyTermination\Foo::class => [ @@ -9909,7 +9280,7 @@ protected function getEarlyTerminatingMethodCalls(): array ]; } - protected function getEarlyTerminatingFunctionCalls(): array + protected static function getEarlyTerminatingFunctionCalls(): array { return ['baz']; } @@ -9917,19 +9288,19 @@ protected function getEarlyTerminatingFunctionCalls(): array private function assertTypeDescribe( string $expectedDescription, Type $actualType, - string $label = '' + string $label = '', ): void { $actualDescription = $actualType->describe(VerbosityLevel::precise()); $this->assertSame( $expectedDescription, $actualDescription, - $label + $label, ); } /** @return string[] */ - protected function getAdditionalAnalysedFiles(): array + protected static function getAdditionalAnalysedFiles(): array { return [ __DIR__ . '/data/methodPhpDocs-trait-defined.php', diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php new file mode 100644 index 0000000000..3b4f48d07a --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -0,0 +1,41 @@ +> + */ + public static function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php new file mode 100644 index 0000000000..fae702cec2 --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php @@ -0,0 +1,41 @@ +> + */ + public static function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp8.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b095b3db2a..9f7165c275 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -2,533 +2,296 @@ namespace PHPStan\Analyser; +use EnumTypeAssertions\Foo; +use PHPStan\File\FileHelper; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +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 DIRECTORY_SEPARATOR; +use const PHP_INT_SIZE; +use const PHP_VERSION_ID; class NodeScopeResolverTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + /** + * @return iterable + */ + private static function findTestFiles(): iterable { - require_once __DIR__ . '/data/bug2574.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); - - require_once __DIR__ . '/data/bug2577.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2577.php'); - - require_once __DIR__ . '/data/generics.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics.php'); - - require_once __DIR__ . '/data/generic-class-string.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-class-string.php'); - - require_once __DIR__ . '/data/instanceof.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2612.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2676.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/psalm-prefix-unresolvable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/complex-generics-example.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2648.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2740.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-parameter-remapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-constructors.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/list-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2835.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2443.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/type-change-after-array-access-assignment.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator_to_array.php'); - - if (self::$useStaticReflectionProvider || extension_loaded('ds')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/ext-ds.php'); - } - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70401) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-return-type.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/is-a.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3226.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2001.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2232.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3009.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-var.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-param.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-return.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-template.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3266.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3269.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assign-nested-arrays.php'); - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3276.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/shadowed-trait-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions-namespaced.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/root-scope-maybe-defined.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3336.php'); - } - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/catch-without-variable.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-typehint.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-typehint-without-null-in-phpdoc.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/override-root-scope-variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bitwise-not.php'); - if (extension_loaded('gd')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/graphics-draw-return-types.php'); + foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) { + yield $testFile; } - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; + if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/data/enum-reflection-php81.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/unionTypes.php'); + if (PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80400) { + yield __DIR__ . '/data/enum-reflection-backed.php'; } - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/mixedType.php'); + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/bug-4902.php'; } - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/staticReturnType.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'; + } } - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3875.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2611.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3548.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3866.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1014.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-pr-339.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/pow.php'); - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php'); + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/data/str-split-php82.php'; + } elseif (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/data/str-split-php80.php'; + } else { + yield __DIR__ . '/data/str-split-php74.php'; + } + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/data/mb-str-split-php82.php'; + } elseif (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/data/mb-str-split-php80.php'; + } elseif (PHP_VERSION_ID >= 74000) { + yield __DIR__ . '/data/mb-str-split-php74.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-6856.php'; - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-on-expr.php'); + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/explode-php74.php'; + } else { + yield __DIR__ . '/data/explode-php80.php'; } if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961.php'); + yield __DIR__ . '/../Reflection/data/unionTypes.php'; + yield __DIR__ . '/../Reflection/data/mixedType.php'; + yield __DIR__ . '/../Reflection/data/staticReturnType.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1924.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-int-types.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/count-type.php'); + if (PHP_INT_SIZE === 8) { + yield __DIR__ . '/data/predefined-constants-64bit.php'; + } else { + yield __DIR__ . '/data/predefined-constants-32bit.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.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/Comparison/data/bug-12412.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'; + yield __DIR__ . '/../Rules/Arrays/data/bug-13538.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-4857.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3985.php'); + 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/new-in-initializers-runtime.php'; + yield __DIR__ . '/data/scope-in-enum-match-arm-body.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-slice.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3990.php'); + yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3991.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-5749.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5757.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3993.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Methods/data/bug-6635.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php'); + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Constants/data/bug-10212.php'; + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-3284.php'; - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/promoted-properties-types.php'); + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/early-termination-phpdoc.php'); + //define('ALREADY_DEFINED_CONSTANT', true); + //yield from self::gatherAssertTypes(__DIR__ . '/data/already-defined-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3915.php'); + yield __DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2378.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-7511.php'; + yield __DIR__ . '/../Rules/Properties/data/trait-mixin.php'; + yield __DIR__ . '/../Rules/Methods/data/trait-mixin.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-4708.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-7156.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-6364.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-5758.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-3931.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-7417.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-7469.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-3391.php'; - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expr.php'); - } - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe.php'); - } + yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-use.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/cast-to-numeric-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2539.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3132.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1233.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/comparison-operators.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3880.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inc-dec-in-conditions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4099.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3760.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2997.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4207.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4206.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4205.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variable-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1865.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-non-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-dependent-key-value.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-type-guard-same-as-type.php'); - - if (PHP_VERSION_ID >= 70400 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-arrow-function.php'); + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/../Rules/Methods/data/true-typehint.php'; } + yield __DIR__ . '/../Rules/Arrays/data/bug-6000.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2980.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3986.php'); + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'; + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'; - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4188.php'); - } + yield __DIR__ . '/../Rules/Methods/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php'; - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4339.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4343.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-method.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-use.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4398.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4415.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/compact.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4500.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4504.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4436.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2549.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2003.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-651.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1283.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4538.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/proc_get_status.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4552.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1897.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2927.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4557.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209-2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2869.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3024.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3134.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/infer-array-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-value-after-assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2112.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map-closure.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-sum.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4573.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4577.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4579.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php'); - - require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof-class-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4498.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4587.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4606.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3922.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping-covariant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-incomplete-constructor.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator-iterator.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4642.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-4643.php'); - require_once __DIR__ . '/data/throw-points/helpers.php'; if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/php8/null-safe-method-call.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/and.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array-dim-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign-op.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/do-while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/for.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/foreach.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/func-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/if.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/method-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/or.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/property-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/static-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/switch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/throw.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch-finally.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-override.php'); - require_once __DIR__ . '/data/phpdoc-pseudotype-namespace.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-namespace.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-unions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-parent.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4247.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4267.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4213.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4707.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4545.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4714.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4725.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4326.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-987.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4215.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4695.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2977.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ternary-specified-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-560.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/do-not-remember-impure-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/clear-stat-cache.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-static.php'); - - require_once __DIR__ . '/data/invalidate-object-argument-function.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-function.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4588.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4091.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3382.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4177.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2288.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1157.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1597.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3617.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-778.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2969.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3004.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3710.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-505.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1670.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1219.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3302.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1511.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4434.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4287.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4700.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-in-closure-bind.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php'); - - require_once __DIR__ . '/data/type-aliases.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4650.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2906.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeDynamicReturnTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4821.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4838.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4879.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4820.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4816.php'); - - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4757.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-7898.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4814.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4982.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4761.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3331.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3106.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2640.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2413.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3446.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/getopt.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-default.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4985.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/number_format.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5140.php'); - - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4857.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Functions/data/bug-7823.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/empty-array-shape.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5089.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3158.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'); + yield __DIR__ . '/../Analyser/data/is-resource-specified.php'; - require_once __DIR__ . '/../Rules/Functions/data/varying-acceptor.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/varying-acceptor.php'); + yield __DIR__ . '/../Rules/Arrays/data/bug-7954.php'; + yield __DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'; + yield __DIR__ . '/../Rules/Properties/data/bug-7839.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-5333.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-8174.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/uksort-bug.php'); + yield __DIR__ . '/../Rules/Properties/data/bug-13537.php'; - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8169.php'; } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3981.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4711.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/sscanf.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-offset-get.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-object-lower-bound.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-reflection-interfaces.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4415.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5259.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5293.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5129.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4970.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5322.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/splfixedarray-iterator-types.php'); - - if (PHP_VERSION_ID >= 70400 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php'); + yield __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'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php8.php'); - } elseif (PHP_VERSION_ID < 70200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php71.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character.php'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-9007.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php'); + yield __DIR__ . '/../Rules/DeadCode/data/bug-8620.php'; - if (self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/../Rules/Constants/data/bug-8957.php'; } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/reflectionclass-issue-5511-php8.php'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-9499.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/modulo-operator.php'); + yield __DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-5365.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-6551.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-9403.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-9542.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-9803.php'; + yield __DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591-method-tag.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; + yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; + + yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; + yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; + } + + /** + * @return iterable + */ + public static function dataFile(): iterable + { + $base = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR; + $baseLength = strlen($base); - yield from $this->gatherAssertTypes(__DIR__ . '/data/literal-string.php'); + $fileHelper = new FileHelper($base); + foreach (self::findTestFiles() as $file) { + $file = $fileHelper->normalizePath($file); - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-returns-non-empty-string.php'); + $testName = $file; + if (str_starts_with($file, $base)) { + $testName = substr($file, $baseLength); + } - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/model-mixin.php'); + yield $testName => [$file]; } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5529.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4602.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4499.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2142.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5584.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/math.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1870.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5615.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_map_multiple.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/range-numeric-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/missing-closure-native-return-typehint.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4741.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/more-type-strings.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/eval-implicit-throw.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5628.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5501.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4743.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2760.php'); } - /** - * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file - * @param mixed ...$args - */ - public function testFileAsserts( - string $assertType, - string $file, - ...$args - ): void + #[DataProvider('dataFile')] + public function testFile(string $file): void { - $this->assertFileAsserts($assertType, $file, ...$args); + $asserts = self::gatherAssertTypes($file); + $this->assertNotCount(0, $asserts, sprintf('File %s has no asserts.', $file)); + $failures = []; + + foreach ($asserts as $args) { + $assertType = array_shift($args); + $file = array_shift($args); + + if ($assertType === 'type') { + $expected = $args[0]; + $actual = $args[1]; + + if ($expected !== $actual) { + $failures[] = sprintf("Line %d:\nExpected: %s\nActual: %s\n", $args[2], $expected, $actual); + } + } elseif ($assertType === 'variableCertainty') { + $expectedCertainty = $args[0]; + $actualCertainty = $args[1]; + $variableName = $args[2]; + + if ($expectedCertainty->equals($actualCertainty) !== true) { + $failures[] = sprintf("Certainty of %s on line %d:\nExpected: %s\nActual: %s\n", $variableName, $args[3], $expectedCertainty->describe(), $actualCertainty->describe()); + } + } + } + + if ($failures === []) { + return; + } + + self::fail(sprintf("Failed assertions in %s:\n\n%s", $file, implode("\n", $failures))); } public static function getAdditionalConfigFiles(): array diff --git a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php new file mode 100644 index 0000000000..cd28ff80f7 --- /dev/null +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -0,0 +1,36 @@ +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..251479a95b --- /dev/null +++ b/tests/PHPStan/Analyser/ParamOutTypeTest.php @@ -0,0 +1,38 @@ +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/ParameterClosureThisExtensionTest.php b/tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php new file mode 100644 index 0000000000..fed715244f --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php @@ -0,0 +1,32 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/parameter-closure-this-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php new file mode 100644 index 0000000000..778dbf90f7 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php @@ -0,0 +1,36 @@ +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..6079115500 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php @@ -0,0 +1,36 @@ +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..3ad3d0e1f0 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php @@ -0,0 +1,36 @@ +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..3a3e124e58 --- /dev/null +++ b/tests/PHPStan/Analyser/PathConstantsTest.php @@ -0,0 +1,41 @@ +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..193a55c53c --- /dev/null +++ b/tests/PHPStan/Analyser/ScopePhpVersionTest.php @@ -0,0 +1,42 @@ +', + __DIR__ . '/data/scope-constants-global.php', + ], + [ + 'int<80000, 80599>', + __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 c6509673e0..e83ed19354 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -5,19 +5,23 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name\FullyQualified; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; class ScopeTest extends PHPStanTestCase { - public function dataGeneralize(): array + public static function dataGeneralize(): array { return [ [ @@ -28,12 +32,12 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'literal-string&non-empty-string', + 'literal-string&lowercase-string&non-falsy-string', ], [ new ConstantIntegerType(0), new ConstantIntegerType(1), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -45,7 +49,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -72,7 +76,7 @@ public function dataGeneralize(): array new ConstantIntegerType(2), new ConstantStringType('foo'), ]), - '\'foo\'|int', + '\'foo\'|int<0, max>', ], [ new ConstantBooleanType(false), @@ -93,7 +97,7 @@ public function dataGeneralize(): array [ new ObjectType('Foo'), new ConstantBooleanType(false), - 'Foo', + 'Foo|false', ], [ new ConstantArrayType([ @@ -106,7 +110,7 @@ public function dataGeneralize(): array ], [ new ConstantIntegerType(1), ]), - 'array(\'a\' => 1)', + 'array{a: 1}', ], [ new ConstantArrayType([ @@ -123,7 +127,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 +142,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -153,23 +157,84 @@ 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 - */ + #[DataProvider('dataGeneralize')] 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 +246,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 a0f71eccde..680f528815 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -4,15 +4,19 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class StatementResultTest extends \PHPStan\Testing\PHPStanTestCase +class StatementResultTest extends PHPStanTestCase { - public function dataIsAlwaysTerminating(): array + public static function dataIsAlwaysTerminating(): array { return [ [ @@ -171,6 +175,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, @@ -229,7 +365,7 @@ public function dataIsAlwaysTerminating(): array ], [ 'for ($i = 0; $i < 10; $i++) { return; }', - false, // will be true with range types + true, ], [ 'for ($i = 0; $i < 0; $i++) { return; }', @@ -374,14 +510,10 @@ public function dataIsAlwaysTerminating(): array ]; } - /** - * @dataProvider dataIsAlwaysTerminating - * @param string $code - * @param bool $expectedIsAlwaysTerminating - */ + #[DataProvider('dataIsAlwaysTerminating')] public function testIsAlwaysTerminating( string $code, - bool $expectedIsAlwaysTerminating + bool $expectedIsAlwaysTerminating, ): void { /** @var Parser $parser */ @@ -395,16 +527,17 @@ public function testIsAlwaysTerminating( /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('test.php')) - ->assignVariable('string', new StringType()) - ->assignVariable('x', new IntegerType()) - ->assignVariable('cond', new MixedType()) - ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType())); + ->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('cond', new MixedType(), new MixedType(), TrinaryLogic::createYes()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); $result = $nodeScopeResolver->processStmtNodes( - new Stmt\Namespace_(null, $stmts), + new Stmt\Namespace_(stmts: $stmts), $stmts, $scope, static function (): void { - } + }, + StatementContext::createTopLevel(), ); $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); } diff --git a/tests/PHPStan/Analyser/SubstrPhp7Test.php b/tests/PHPStan/Analyser/SubstrPhp7Test.php new file mode 100644 index 0000000000..561b33500c --- /dev/null +++ b/tests/PHPStan/Analyser/SubstrPhp7Test.php @@ -0,0 +1,39 @@ +> + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php7.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/SubstrPhp8Test.php b/tests/PHPStan/Analyser/SubstrPhp8Test.php new file mode 100644 index 0000000000..934a7ac78c --- /dev/null +++ b/tests/PHPStan/Analyser/SubstrPhp8Test.php @@ -0,0 +1,39 @@ +> + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php8.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp8.neon', + ]; + } + +} 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..a57c784dd9 --- /dev/null +++ b/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php @@ -0,0 +1,37 @@ +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..097279e6d3 --- /dev/null +++ b/tests/PHPStan/Analyser/TraitStubFilesTest.php @@ -0,0 +1,36 @@ +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 d7931f98e6..10c28510a5 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php @@ -2,10 +2,14 @@ namespace PHPStan\Analyser; -class TypeSpecifierContextTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class TypeSpecifierContextTest extends PHPStanTestCase { - public function dataContext(): array + public static function dataContext(): array { return [ [ @@ -32,10 +36,9 @@ public function dataContext(): array } /** - * @dataProvider dataContext - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ + #[DataProvider('dataContext')] public function testContext(TypeSpecifierContext $context, array $results): void { $this->assertSame($results[0], $context->true()); @@ -45,7 +48,7 @@ public function testContext(TypeSpecifierContext $context, array $results): void $this->assertSame($results[4], $context->null()); } - public function dataNegate(): array + public static function dataNegate(): array { return [ [ @@ -68,10 +71,9 @@ public function dataNegate(): array } /** - * @dataProvider dataNegate - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ + #[DataProvider('dataNegate')] public function testNegate(TypeSpecifierContext $context, array $results): void { $this->assertSame($results[0], $context->true()); @@ -83,7 +85,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 b6b46a0ba3..5a06bfb0aa 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Analyser; +use Override; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Equal; 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,59 +18,75 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +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\PHPStanTestCase +class TypeSpecifierTest extends PHPStanTestCase { - private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array()|false|null'; - private const TRUTHY_TYPE_DESCRIPTION = 'mixed~' . self::FALSEY_TYPE_DESCRIPTION; + private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; + private const TRUTHY_TYPE_DESCRIPTION = 'mixed~(' . self::FALSEY_TYPE_DESCRIPTION . ')'; private const SURE_NOT_FALSEY = '~' . self::FALSEY_TYPE_DESCRIPTION; private const SURE_NOT_TRUTHY = '~' . self::TRUTHY_TYPE_DESCRIPTION; - /** @var \PhpParser\PrettyPrinter\Standard() */ - private $printer; + /** @var Standard () */ + private Standard $printer; - /** @var \PHPStan\Analyser\TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; - /** @var Scope */ - private $scope; + private Scope $scope; + #[Override] protected function setUp(): void { - $reflectionProvider = $this->createReflectionProvider(); - $this->printer = new \PhpParser\PrettyPrinter\Standard(); + $reflectionProvider = self::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')); - $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()])); - $this->scope = $this->scope->assignVariable('string', new StringType()); - $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()])); - $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType())); - $this->scope = $this->scope->assignVariable('foo', new MixedType()); - $this->scope = $this->scope->assignVariable('classString', new ClassStringType()); - $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar'))); + $this->scope = $this->scope->assignVariable('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 */ + #[DataProvider('dataCondition')] public function testCondition(Expr $expr, array $expectedPositiveResult, array $expectedNegatedResult): void { $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this->scope, $expr, TypeSpecifierContext::createTruthy()); @@ -80,97 +98,129 @@ 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 static 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'), + self::createFunctionCall('is_int'), ['$foo' => 'int'], ['$foo' => '~int'], ], [ - $this->createFunctionCall('is_numeric'), - ['$foo' => 'float|int|(string&numeric)'], - ['$foo' => '~float|int'], + self::createFunctionCall('is_numeric'), + ['$foo' => 'float|int|numeric-string'], + ['$foo' => '~float|int|numeric-string'], ], [ - $this->createFunctionCall('is_scalar'), + self::createFunctionCall('is_scalar'), ['$foo' => 'bool|float|int|string'], ['$foo' => '~bool|float|int|string'], ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + self::createFunctionCall('is_int'), + self::createFunctionCall('random'), ), ['$foo' => 'int'], [], ], [ new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + self::createFunctionCall('is_int'), + self::createFunctionCall('random'), ), [], ['$foo' => '~int'], ], [ new Expr\BinaryOp\LogicalAnd( - $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + self::createFunctionCall('is_int'), + self::createFunctionCall('random'), ), ['$foo' => 'int'], [], ], [ new Expr\BinaryOp\LogicalOr( - $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + self::createFunctionCall('is_int'), + self::createFunctionCall('random'), ), [], ['$foo' => '~int'], ], [ - new Expr\BooleanNot($this->createFunctionCall('is_int')), + new Expr\BooleanNot(self::createFunctionCall('is_int')), ['$foo' => '~int'], ['$foo' => 'int'], ], [ new Expr\BinaryOp\BooleanAnd( - new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + new Expr\BooleanNot(self::createFunctionCall('is_int')), + self::createFunctionCall('random'), ), ['$foo' => '~int'], [], ], [ new Expr\BinaryOp\BooleanOr( - new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + new Expr\BooleanNot(self::createFunctionCall('is_int')), + self::createFunctionCall('random'), ), [], ['$foo' => 'int'], ], [ - new Expr\BooleanNot(new Expr\BooleanNot($this->createFunctionCall('is_int'))), + new Expr\BooleanNot(new Expr\BooleanNot(self::createFunctionCall('is_int'))), ['$foo' => 'int'], ['$foo' => '~int'], ], [ - $this->createInstanceOf('Foo'), + self::createInstanceOf('Foo'), ['$foo' => 'Foo'], ['$foo' => '~Foo'], ], [ - new Expr\BooleanNot($this->createInstanceOf('Foo')), + new Expr\BooleanNot(self::createInstanceOf('Foo')), ['$foo' => '~Foo'], ['$foo' => 'Foo'], ], [ new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), ['$foo' => 'object'], [], @@ -180,27 +230,47 @@ 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'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + new String_('Foo'), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new String_('Foo'), + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], ], [ new BooleanNot( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') - ) + new Variable('className'), + ), ), [], ['$foo' => 'object'], @@ -213,7 +283,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new Variable('foo'), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), ['$foo' => self::SURE_NOT_FALSEY], [], @@ -221,7 +291,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new Variable('foo'), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), [], ['$foo' => self::SURE_NOT_TRUTHY], @@ -240,7 +310,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), ['$this->foo' => self::SURE_NOT_FALSEY], [], @@ -248,7 +318,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), [], ['$this->foo' => self::SURE_NOT_TRUTHY], @@ -261,27 +331,27 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_int'), - $this->createFunctionCall('is_string') + self::createFunctionCall('is_int'), + self::createFunctionCall('is_string'), ), ['$foo' => 'int|string'], ['$foo' => '~int|string'], ], [ new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_string'), - $this->createFunctionCall('is_bool') - ) + self::createFunctionCall('is_string'), + self::createFunctionCall('is_bool'), + ), ), ['$foo' => 'bool|int|string'], ['$foo' => '~bool|int|string'], ], [ new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + self::createFunctionCall('is_int', 'foo'), + self::createFunctionCall('is_string', 'bar'), ), [], ['$foo' => '~int', '$bar' => '~string'], @@ -289,10 +359,10 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( - $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + self::createFunctionCall('is_int', 'foo'), + self::createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), ['$foo' => 'int|string'], [], @@ -300,21 +370,21 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + self::createFunctionCall('is_int', 'foo'), + self::createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), [], - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], ], [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + self::createFunctionCall('is_int', 'foo'), + self::createFunctionCall('is_string', 'bar'), ), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), [], [], @@ -322,10 +392,10 @@ 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(self::createFunctionCall('is_int', 'foo')), + new Expr\BooleanNot(self::createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), [], ['$foo' => 'int|string'], @@ -333,19 +403,19 @@ 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(self::createFunctionCall('is_int', 'foo')), + new Expr\BooleanNot(self::createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + self::createFunctionCall('random'), ), - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], [], ], [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'true & ~' . self::FALSEY_TYPE_DESCRIPTION], ['$foo' => '~true'], @@ -353,39 +423,47 @@ public function dataCondition(): array [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => 'false & ~' . self::TRUTHY_TYPE_DESCRIPTION], ['$foo' => '~false'], ], [ new Identical( - $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + self::createFunctionCall('is_int'), + new Expr\ConstFetch(new Name('true')), ), ['is_int($foo)' => 'true', '$foo' => 'int'], ['is_int($foo)' => '~true', '$foo' => '~int'], ], [ new Identical( - $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('false')) + self::createFunctionCall('is_string'), + new Expr\ConstFetch(new Name('true')), + ), + ['is_string($foo)' => 'true', '$foo' => 'string'], + ['is_string($foo)' => '~true', '$foo' => '~string'], + ], + [ + new Identical( + self::createFunctionCall('is_int'), + new Expr\ConstFetch(new Name('false')), ), ['is_int($foo)' => 'false', '$foo' => '~int'], ['$foo' => 'int', 'is_int($foo)' => '~false'], ], [ new Equal( - $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + self::createFunctionCall('is_int'), + new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'int'], ['$foo' => '~int'], ], [ new Equal( - $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('false')) + self::createFunctionCall('is_int'), + new Expr\ConstFetch(new Name('false')), ), ['$foo' => '~int'], ['$foo' => 'int'], @@ -393,7 +471,7 @@ public function dataCondition(): array [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => self::SURE_NOT_TRUTHY], ['$foo' => self::SURE_NOT_FALSEY], @@ -401,17 +479,17 @@ public function dataCondition(): array [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), - ['$foo' => self::SURE_NOT_TRUTHY], - ['$foo' => self::SURE_NOT_FALSEY], + ['$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' [], ], [ @@ -435,11 +513,11 @@ public function dataCondition(): array new Arg(new Variable('foo')), new Arg(new Expr\ClassConstFetch( new Name('static'), - 'class' + 'class', )), ]), ['$foo' => 'static(DateTime)'], - ['$foo' => '~static(DateTime)'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -455,7 +533,7 @@ public function dataCondition(): array new Arg(new Variable('genericClassString')), ]), ['$foo' => 'Bar'], - ['$foo' => '~Bar'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -464,7 +542,7 @@ public function dataCondition(): array new Arg(new Expr\ConstFetch(new Name('true'))), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -472,7 +550,7 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Expr\ConstFetch(new Name('true'))), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ @@ -482,7 +560,7 @@ public function dataCondition(): array new Arg(new Variable('unknown')), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -490,13 +568,13 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Variable('unknown')), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrNull') + new Variable('stringOrNull'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], @@ -504,7 +582,7 @@ public function dataCondition(): array [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrFalse') + new Variable('stringOrFalse'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], @@ -512,11 +590,22 @@ public function dataCondition(): array [ new Expr\Assign( new Variable('foo'), - new Variable('bar') + new Variable('bar'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + ]), + [ + '$stringOrNull' => '~null', + ], + [ + '$stringOrNull' => 'null', + ], + ], [ new Expr\Isset_([ new Variable('stringOrNull'), @@ -526,51 +615,67 @@ public function dataCondition(): array '$stringOrNull' => '~null', '$barOrNull' => '~null', ], + [], + ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + new Variable('barOrNull'), + new Variable('fooOrNull'), + ]), [ - 'isset($stringOrNull, $barOrNull)' => self::SURE_NOT_TRUTHY, + '$stringOrNull' => '~null', + '$barOrNull' => '~null', + '$fooOrNull' => '~null', ], + [], ], [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), [ - '$stringOrNull' => '~0|0.0|\'\'|\'0\'|array()|false|null', + '$stringOrNull' => '~0|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' => '~0|0.0|\'\'|\'0\'|array()|false|null', + '$array' => 'array{}|null', + ], + [ + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], ], [ new BooleanNot(new Expr\Empty_(new Variable('array'))), [ - '$array' => '~0|0.0|\'\'|\'0\'|array()|false|null', + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + ], + [ + '$array' => 'array{}|null', ], - [], ], [ new FuncCall(new Name('count'), [ new Arg(new Variable('array')), ]), [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], ], [ @@ -578,10 +683,10 @@ public function dataCondition(): array new Arg(new Variable('array')), ])), [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], ], [ @@ -589,10 +694,10 @@ public function dataCondition(): array new Arg(new Variable('array')), ]), [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], ], [ @@ -600,10 +705,10 @@ public function dataCondition(): array new Arg(new Variable('array')), ])), [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], ], [ @@ -628,9 +733,9 @@ public function dataCondition(): array new Equal( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), - new LNumber(1) + new LNumber(1), ), ['$foo' => 'object'], [], @@ -639,9 +744,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), ), [], [ @@ -652,33 +757,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)' => self::SURE_NOT_TRUTHY, - ], + [], ], [ new Expr\Isset_( [ new Expr\StaticPropertyFetch(new Name('Foo'), new VarLikeIdentifier('bar')), - ] + ], ), [ 'Foo::$bar' => '~null', ], - [ - 'isset(Foo::$bar)' => self::SURE_NOT_TRUTHY, - ], + [], ], [ new Identical( new Variable('barOrNull'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$barOrNull' => 'null', @@ -691,21 +792,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', @@ -717,34 +820,34 @@ 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~(int<3, max>|true)', ], [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~(0.0|int|false|null)', ], ], [ new Expr\BinaryOp\Smaller( new Variable('n'), - new LNumber(PHP_INT_MIN) + new LNumber(PHP_INT_MIN), ), [ - '$n' => 'mixed~int<' . PHP_INT_MIN . ', max>|true', + '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', ], [ - '$n' => 'mixed~false|null', + '$n' => 'mixed~(0.0|false|null)', ], ], [ new Expr\BinaryOp\Greater( new Variable('n'), - new LNumber(PHP_INT_MAX) + new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], [ '$n' => 'mixed', @@ -753,55 +856,55 @@ public function dataCondition(): array [ new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(PHP_INT_MIN) + new LNumber(PHP_INT_MIN), ), [ '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', ], [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], ], [ new Expr\BinaryOp\GreaterOrEqual( new Variable('n'), - new LNumber(PHP_INT_MAX) + new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~(0.0|int|false|null)', ], [ - '$n' => 'mixed~int<' . PHP_INT_MAX . ', max>|true', + '$n' => 'mixed~(int<' . PHP_INT_MAX . ', max>|true)', ], ], [ new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\GreaterOrEqual( new Variable('n'), - new LNumber(3) + new LNumber(3), ), new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(5) - ) + new LNumber(5), + ), ), [ - '$n' => 'mixed~int|int<6, max>|false|null', + '$n' => 'mixed~(0.0|int|int<6, max>|false|null)', ], [ - '$n' => 'mixed~int<3, 5>|true', + '$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), + ), ), [ '$n' => 'mixed~int<6, max>', @@ -813,21 +916,23 @@ 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 & ' . self::SURE_NOT_TRUTHY, @@ -840,21 +945,44 @@ public function dataCondition(): array new Identical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$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', @@ -867,30 +995,34 @@ public function dataCondition(): array new NotIdentical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', ], [ '$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', ], ], [ @@ -902,10 +1034,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\')', @@ -920,13 +1052,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', ], ], [ @@ -935,7 +1067,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\')', @@ -951,6 +1131,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')), @@ -962,11 +1151,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( + self::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( + self::createFunctionCall('is_string', 'a'), + new Expr\BinaryOp\Greater( + self::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( + self::createFunctionCall('is_array', 'a'), + new Expr\BinaryOp\Greater( + self::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( + self::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( + self::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( + self::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( + self::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( + self::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( + self::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 @@ -989,12 +1346,18 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array return $descriptions; } - private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ + /** + * @param non-empty-string $className + */ + private static function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ { return new Expr\Instanceof_(new Variable($variableName), new Name($className)); } - private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall + /** + * @param non-empty-string $functionName + */ + private static 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/TypeSpecifyingExtensionTypeInferenceFalseTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php index e04154b9e2..8b351b2a57 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php @@ -3,27 +3,26 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TypeSpecifyingExtensionTypeInferenceFalseTest extends TypeInferenceTestCase { - public function dataTypeSpecifyingExtensionsFalse(): iterable + public static function dataTypeSpecifyingExtensionsFalse(): iterable { - yield from $this->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'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-false.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-false.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-false.php'); } /** - * @dataProvider dataTypeSpecifyingExtensionsFalse - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsFalse')] public function testTypeSpecifyingExtensionsFalse( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php index 2084a79701..de7d4a7e4e 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php @@ -3,27 +3,26 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TypeSpecifyingExtensionTypeInferenceNullTest extends TypeInferenceTestCase { - public function dataTypeSpecifyingExtensionsNull(): iterable + public static function dataTypeSpecifyingExtensionsNull(): iterable { - yield from $this->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'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-null.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-null.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-null.php'); } /** - * @dataProvider dataTypeSpecifyingExtensionsNull - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsNull')] public function testTypeSpecifyingExtensionsNull( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php index 6f0cf48bfb..25a9e8a0b2 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php @@ -3,27 +3,26 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TypeSpecifyingExtensionTypeInferenceTrueTest extends TypeInferenceTestCase { - public function dataTypeSpecifyingExtensionsTrue(): iterable + public static function dataTypeSpecifyingExtensionsTrue(): iterable { - yield from $this->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'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-true.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-true.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-true.php'); } /** - * @dataProvider dataTypeSpecifyingExtensionsTrue - * @param string $assertType - * @param string $file * @param mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsTrue')] public function testTypeSpecifyingExtensionsTrue( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); 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/checkDynamicProperties.neon b/tests/PHPStan/Analyser/checkDynamicProperties.neon new file mode 100644 index 0000000000..a5e54a7f73 --- /dev/null +++ b/tests/PHPStan/Analyser/checkDynamicProperties.neon @@ -0,0 +1,2 @@ +parameters: + checkDynamicProperties: true diff --git a/tests/PHPStan/Analyser/conditional-return-type-stub.neon b/tests/PHPStan/Analyser/conditional-return-type-stub.neon new file mode 100644 index 0000000000..9fe5ad41fe --- /dev/null +++ b/tests/PHPStan/Analyser/conditional-return-type-stub.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/conditional-return-type.stub diff --git a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php index 3ee8656661..b66f99e76b 100644 --- a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php +++ b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php @@ -1,7 +1,7 @@ name instanceof Identifier) { + return null; + } + + if ($expr->name->name !== 'methodReturningBoolNoMatterTheCallerUnlessReturnsString') { + return null; + } + + $methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->name); + + if ($methodReflection === null) { + return null; + } + + $returnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants() + )->getReturnType(); + + if ($returnType instanceof StringType) { + return null; + } + + return new BooleanType(); + } + +} diff --git a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php index 3fd9f8b0b4..dd72c4525e 100644 --- a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -2,17 +2,27 @@ namespace PHPStan\Tests; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\UnionTypeMethodReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class GetByPrimaryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -31,16 +41,28 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $arg = $args[0]->value; if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType((string) $arg->class); @@ -65,12 +87,20 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $argType = $scope->getType($args[0]->value); if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType($argType->getValue()); @@ -95,16 +125,28 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $arg = $args[0]->value; if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType((string) $arg->class); @@ -189,3 +231,71 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } + + +class ConditionalGetSingle implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnGetSingleConditional\Foo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + +} + +class Bug7344DynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + public function getClass(): string + { + return \Bug7344\Model::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getModel'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + return new IntegerType(); + } + +} + +class Bug7391BDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + public function getClass(): string + { + return \Bug7391B\Foo::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'm'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + // return instantiated type from class string + return $scope->getType(new New_($methodCall->class)); + } +} diff --git a/tests/PHPStan/Analyser/data/already-defined-constant.php b/tests/PHPStan/Analyser/data/already-defined-constant.php new file mode 100644 index 0000000000..e37c747d22 --- /dev/null +++ b/tests/PHPStan/Analyser/data/already-defined-constant.php @@ -0,0 +1,15 @@ + true, 'value' => '123']; diff --git a/tests/PHPStan/Analyser/data/array-flip.php b/tests/PHPStan/Analyser/data/array-flip.php deleted file mode 100644 index 2275170d0d..0000000000 --- a/tests/PHPStan/Analyser/data/array-flip.php +++ /dev/null @@ -1,43 +0,0 @@ -', $flip); -} - -/** - * @param mixed[] $list - */ -function foo3($list) -{ - $flip = array_flip($list); - - assertType('array', $flip); -} - -/** - * @param array $array - */ -function foo4($array) -{ - $flip = array_flip($array); - assertType('array<1|2|3, int>', $flip); -} - - -/** - * @param array<1|2|3, string> $array - */ -function foo5($array) -{ - $flip = array_flip($array); - assertType('array', $flip); -} diff --git a/tests/PHPStan/Analyser/data/array-map.php b/tests/PHPStan/Analyser/data/array-map.php deleted file mode 100644 index 97767fb116..0000000000 --- a/tests/PHPStan/Analyser/data/array-map.php +++ /dev/null @@ -1,62 +0,0 @@ - $array - */ -function foo(array $array): void { - $mapped = array_map( - static function(string $string): string { - return (string) $string; - }, - $array - ); - - assertType('array', $mapped); -} - -/** - * @param non-empty-array $array - */ -function foo2(array $array): void { - $mapped = array_map( - static function(string $string): string { - return (string) $string; - }, - $array - ); - - assertType('array&nonEmpty', $mapped); -} - -/** - * @param list $array - */ -function foo3(array $array): void { - $mapped = array_map( - static function(string $string): string { - return (string) $string; - }, - $array - ); - - assertType('array', $mapped); -} - -/** - * @param non-empty-list $array - */ -function foo4(array $array): void { - $mapped = array_map( - static function(string $string): string { - return (string) $string; - }, - $array - ); - - assertType('array&nonEmpty', $mapped); -} diff --git a/tests/PHPStan/Analyser/data/array-pointer-functions.php b/tests/PHPStan/Analyser/data/array-pointer-functions.php index 6bae9731a0..60786d435c 100644 --- a/tests/PHPStan/Analyser/data/array-pointer-functions.php +++ b/tests/PHPStan/Analyser/data/array-pointer-functions.php @@ -2,6 +2,8 @@ namespace ResetDynamicReturnTypeExtension; +use function PHPStan\Testing\assertType; + class Foo { @@ -16,6 +18,12 @@ public function doFoo(array $generalArray, $somethingElse) 'a' => 1, 'b' => 2, ]; + /** @var array{a?: 0, b: 1, c: 2} $constantArrayOptionalKeys1 */ + $constantArrayOptionalKeys1 = []; + /** @var array{a: 0, b?: 1, c: 2} $constantArrayOptionalKeys2 */ + $constantArrayOptionalKeys2 = []; + /** @var array{a: 0, b: 1, c?: 2} $constantArrayOptionalKeys3 */ + $constantArrayOptionalKeys3 = []; $conditionalArray = ['foo', 'bar']; if (doFoo()) { diff --git a/tests/PHPStan/Analyser/data/array-shapes-keys-strings.php b/tests/PHPStan/Analyser/data/array-shapes-keys-strings.php deleted file mode 100644 index 8d515c924b..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-slice.php b/tests/PHPStan/Analyser/data/array-slice.php deleted file mode 100644 index 291ffbdb9f..0000000000 --- a/tests/PHPStan/Analyser/data/array-slice.php +++ /dev/null @@ -1,38 +0,0 @@ - $arr1 - * @param array $arr2 - */ - public function preserveTypes(array $arr1, array $arr2): void - { - assertType('array', array_slice($arr1, 1, 2)); - assertType('array', array_slice($arr1, 1, 2, true)); - assertType('array', array_slice($arr2, 1, 2)); - assertType('array', array_slice($arr2, 1, 2, true)); - } - -} diff --git a/tests/PHPStan/Analyser/data/array-spread.php b/tests/PHPStan/Analyser/data/array-spread.php index 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-sum.php b/tests/PHPStan/Analyser/data/array-sum.php deleted file mode 100644 index 7e2f17fe7e..0000000000 --- a/tests/PHPStan/Analyser/data/array-sum.php +++ /dev/null @@ -1,50 +0,0 @@ - $floatList - */ -function foo3($floatList) -{ - $sum = array_sum($floatList); - assertType('float', $sum); -} - -/** - * @param mixed[] $list - */ -function foo4($list) -{ - $sum = array_sum($list); - assertType('float|int', $sum); -} - -/** - * @param string[] $list - */ -function foo5($list) -{ - $sum = array_sum($list); - assertType('float|int', $sum); -} diff --git a/tests/PHPStan/Analyser/data/array-union.php b/tests/PHPStan/Analyser/data/array-union.php new file mode 100644 index 0000000000..dd70c5e642 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-union.php @@ -0,0 +1,24 @@ + $i], ['bar' => $s]); - assertType('array&nonEmpty', $result); - } - -} diff --git a/tests/PHPStan/Analyser/data/arrow-function-types.php b/tests/PHPStan/Analyser/data/arrow-function-types.php deleted file mode 100644 index 3ea7bf5f07..0000000000 --- a/tests/PHPStan/Analyser/data/arrow-function-types.php +++ /dev/null @@ -1,44 +0,0 @@ -= 7.4 - -namespace ArrowFunctionTypes; - -use function PHPStan\Testing\assertType; - -class Foo -{ - - /** @var array */ - 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 string, \'bar\' => int)>', $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/data/arrow-functions-inside.php b/tests/PHPStan/Analyser/data/arrow-functions-inside.php index 269e2bf8e7..258b951b63 100644 --- a/tests/PHPStan/Analyser/data/arrow-functions-inside.php +++ b/tests/PHPStan/Analyser/data/arrow-functions-inside.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; + + +function (Bar $b, $x): void { + if ($b->doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert.stub b/tests/PHPStan/Analyser/data/assert.stub new file mode 100644 index 0000000000..eeb68738cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert.stub @@ -0,0 +1,16 @@ + 1, \'baz\' => 2)>&nonEmpty', $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)>&nonEmpty>&nonEmpty', $array); - } - -} diff --git a/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php b/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php deleted file mode 100644 index 485709cb35..0000000000 --- a/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php +++ /dev/null @@ -1,97 +0,0 @@ - + */ +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-12095.php b/tests/PHPStan/Analyser/data/bug-12095.php new file mode 100644 index 0000000000..54c41421fd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12095.php @@ -0,0 +1,15 @@ + 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-12214.php b/tests/PHPStan/Analyser/data/bug-12214.php new file mode 100644 index 0000000000..a2efd62ee5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12214.php @@ -0,0 +1,31 @@ + $test + */ +function test_iterable(iterable $test): void +{ + assertType('iterable<(int|string), mixed>', $test); +} + +/** + * @template T of array + * @param T $test + */ +function test_array(array $test): void +{ + assertType('T of array (function Bug12214\test_array(), argument)', $test); +} + +/** + * @template T of iterable + * @param T $test + */ +function test_generic_iterable(iterable $test): void +{ + assertType('T of iterable<(int|string), mixed> (function Bug12214\test_generic_iterable(), argument)', $test); +} diff --git a/tests/PHPStan/Analyser/data/bug-12327.php b/tests/PHPStan/Analyser/data/bug-12327.php new file mode 100644 index 0000000000..7a61985ff6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12327.php @@ -0,0 +1,18 @@ +', $value); - return iterator_to_array($value); - } - - assertType('mixed~array', $value); - - throw new \LogicException(); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-12512.php b/tests/PHPStan/Analyser/data/bug-12512.php new file mode 100644 index 0000000000..612927a31a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12512.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug12512; + +enum FooBarEnum: string +{ + case CASE_ONE = 'case_one'; + case CASE_TWO = 'case_two'; + case CASE_THREE = 'case_three'; + case CASE_FOUR = 'case_four'; + + public function matchFunction(): string + { + return match ($this) { + self::CASE_ONE => 'one', + self::CASE_TWO => 'two', + default => throw new \Exception( + sprintf('"%s" is not implemented yet', get_debug_type($this)) + ) + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12549.php b/tests/PHPStan/Analyser/data/bug-12549.php new file mode 100644 index 0000000000..e1bd8c5f0c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12549.php @@ -0,0 +1,17 @@ +bar(self::OPTION_ROUNDING_MODE); + } + + private function bar(string $v): void + { + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12627.php b/tests/PHPStan/Analyser/data/bug-12627.php new file mode 100644 index 0000000000..ce75be8225 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12627.php @@ -0,0 +1,17 @@ +b(); + } + + private function b(): void + { + } +} + +$c = new class() {}; diff --git a/tests/PHPStan/Analyser/data/bug-12671.php b/tests/PHPStan/Analyser/data/bug-12671.php new file mode 100644 index 0000000000..8066316400 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12671.php @@ -0,0 +1,1198 @@ + [], + // Angola. + 'AO' => [], + // Argentina. + 'AR' => [ + 'C' => ['Ciudad Autónoma de Buenos Aires', 'Ciudad Autónoma de Buenos Aires', NULL], + 'B' => ['Buenos Aires', 'Buenos Aires', NULL], + 'K' => ['Catamarca', 'Catamarca', NULL], + 'H' => ['Chaco', 'Chaco', NULL], + 'U' => ['Chubut', 'Chubut', NULL], + 'X' => ['Córdoba', 'Córdoba', NULL], + 'W' => ['Corrientes', 'Corrientes', NULL], + 'E' => ['Entre Ríos', 'Entre Ríos', NULL], + 'P' => ['Formosa', 'Formosa', NULL], + 'Y' => ['Jujuy', 'Jujuy', NULL], + 'L' => ['La Pampa', 'La Pampa', NULL], + 'F' => ['La Rioja', 'La Rioja', NULL], + 'M' => ['Mendoza', 'Mendoza', NULL], + 'N' => ['Misiones', 'Misiones', NULL], + 'Q' => ['Neuquén', 'Neuquén', NULL], + 'R' => ['Río Negro', 'Río Negro', NULL], + 'A' => ['Salta', 'Salta', NULL], + 'J' => ['San Juan', 'San Juan', NULL], + 'D' => ['San Luis', 'San Luis', NULL], + 'Z' => ['Santa Cruz', 'Santa Cruz', NULL], + 'S' => ['Santa Fe', 'Santa Fe', NULL], + 'G' => ['Santiago del Estero', 'Santiago del Estero', NULL], + 'V' => ['Tierra del Fuego', 'Tierra del Fuego', NULL], + 'T' => ['Tucumán', 'Tucumán', NULL], + ], + // Austria. + 'AT' => [], + // Australia. + 'AU' => [ + 'ACT' => ['ACT', 'Australian Capital Territory', NULL], + 'NSW' => ['NSW', 'New South Wales', NULL], + 'NT' => ['NT', 'Northern Territory', NULL], + 'QLD' => ['QLD', 'Queensland', NULL], + 'SA' => ['SA', 'South Australia', NULL], + 'TAS' => ['TAS', 'Tasmania', NULL], + 'VIC' => ['VIC', 'Victoria', NULL], + 'WA' => ['WA', 'Western Australia', NULL], + // [ 'JBT', 'Jervis Bay Territory', NULL ], + ], + // Aland Islands. + 'AX' => [], + // Bangladesh. + 'BD' => [], + // Belgium. + 'BE' => [], + // Bulgaria. + 'BG' => [], + // Bahrain. + 'BH' => [], + // Burundi. + 'BI' => [], + // Benin. + 'BJ' => [], + // Bolivia. + 'BO' => [], + // Brazil. + 'BR' => [ + 'AC' => ['AC', 'Acre', NULL], + 'AL' => ['AL', 'Alagoas', NULL], + 'AP' => ['AP', 'Amapá', NULL], + 'AM' => ['AM', 'Amazonas', NULL], + 'BA' => ['BA', 'Bahia', NULL], + 'CE' => ['CE', 'Ceará', NULL], + 'DF' => ['DF', 'Distrito Federal', NULL], + 'ES' => ['ES', 'Espírito Santo', NULL], + 'GO' => ['GO', 'Goiás', NULL], + 'MA' => ['MA', 'Maranhão', NULL], + 'MT' => ['MT', 'Mato Grosso', NULL], + 'MS' => ['MS', 'Mato Grosso do Sul', NULL], + 'MG' => ['MG', 'Minas Gerais', NULL], + 'PA' => ['PA', 'Pará', NULL], + 'PB' => ['PB', 'Paraíba', NULL], + 'PR' => ['PR', 'Paraná', NULL], + 'PE' => ['PE', 'Pernambuco', NULL], + 'PI' => ['PI', 'Piauí', NULL], + 'RJ' => ['RJ', 'Rio de Janeiro', NULL], + 'RN' => ['RN', 'Rio Grande do Norte', NULL], + 'RS' => ['RS', 'Rio Grande do Sul', NULL], + 'RO' => ['RO', 'Rondônia', NULL], + 'RR' => ['RR', 'Roraima', NULL], + 'SC' => ['SC', 'Santa Catarina', NULL], + 'SP' => ['SP', 'São Paulo', NULL], + 'SE' => ['SE', 'Sergipe', NULL], + 'TO' => ['TO', 'Tocantins', NULL], + ], + // Canada. + 'CA' => [ + 'AB' => ['AB', 'Alberta', 'Alberta'], + 'BC' => ['BC', 'British Columbia', 'Colombie-Britannique'], + 'MB' => ['MB', 'Manitoba', 'Manitoba'], + 'NB' => ['NB', 'New Brunswick', 'Nouveau-Brunswick'], + 'NL' => ['NL', 'Newfoundland and Labrador', 'Terre-Neuve-et-Labrador'], + 'NT' => ['NT', 'Northwest Territories', 'Territoires du Nord-Ouest'], + 'NS' => ['NS', 'Nova Scotia', 'Nouvelle-Écosse'], + 'NU' => ['NU', 'Nunavut', 'Nunavut'], + 'ON' => ['ON', 'Ontario', 'Ontario'], + 'PE' => ['PE', 'Prince Edward Island', 'Île-du-Prince-Édouard'], + 'QC' => ['QC', 'Quebec', 'Québec'], + 'SK' => ['SK', 'Saskatchewan', 'Saskatchewan'], + 'YT' => ['YT', 'Yukon', 'Yukon'], + ], + // Switzerland. + 'CH' => [], + // China. + 'CN' => [ + 'CN1' => ['Yunnan Sheng', 'Yunnan Sheng', '云南省'], + 'CN2' => ['Beijing Shi', 'Beijing Shi', '北京市'], + 'CN3' => ['Tianjin Shi', 'Tianjin Shi', '天津市'], + 'CN4' => ['Hebei Sheng', 'Hebei Sheng', '河北省'], + 'CN5' => ['Shanxi Sheng', 'Shanxi Sheng', '山西省'], + 'CN6' => ['Neimenggu Zizhiqu', 'Neimenggu Zizhiqu', '内蒙古'], + 'CN7' => ['Liaoning Sheng', 'Liaoning Sheng', '辽宁省'], + 'CN8' => ['Jilin Sheng', 'Jilin Sheng', '吉林省'], + 'CN9' => ['Heilongjiang Sheng', 'Heilongjiang Sheng', '黑龙江省'], + 'CN10' => ['Shanghai Shi', 'Shanghai Shi', '上海市'], + 'CN11' => ['Jiangsu Sheng', 'Jiangsu Sheng', '江苏省'], + 'CN12' => ['Zhejiang Sheng', 'Zhejiang Sheng', '浙江省'], + 'CN13' => ['Anhui Sheng', 'Anhui Sheng', '安徽省'], + 'CN14' => ['Fujian Sheng', 'Fujian Sheng', '福建省'], + 'CN15' => ['Jiangxi Sheng', 'Jiangxi Sheng', '江西省'], + 'CN16' => ['Shandong Sheng', 'Shandong Sheng', '山东省'], + 'CN17' => ['Henan Sheng', 'Henan Sheng', '河南省'], + 'CN18' => ['Hubei Sheng', 'Hubei Sheng', '湖北省'], + 'CN19' => ['Hunan Sheng', 'Hunan Sheng', '湖南省'], + 'CN20' => ['Guangdong Sheng', 'Guangdong Sheng', '广东省'], + 'CN21' => ['Guangxi Zhuangzuzizhiqu', 'Guangxi Zhuangzuzizhiqu', '广西'], + 'CN22' => ['Hainan Sheng', 'Hainan Sheng', '海南省'], + 'CN23' => ['Chongqing Shi', 'Chongqing Shi', '重庆市'], + 'CN24' => ['Sichuan Sheng', 'Sichuan Sheng', '四川省'], + 'CN25' => ['Guizhou Sheng', 'Guizhou Sheng', '贵州省'], + 'CN26' => ['Shaanxi Sheng', 'Shaanxi Sheng', '陕西省'], + 'CN27' => ['Gansu Sheng', 'Gansu Sheng', '甘肃省'], + 'CN28' => ['Qinghai Sheng', 'Qinghai Sheng', '青海省'], + 'CN29' => ['Ningxia Huizuzizhiqu', 'Ningxia Huizuzizhiqu', '宁夏'], + 'CN30' => ['Macau', 'Macau', '澳门'], + 'CN31' => ['Xizang Zizhiqu', 'Xizang Zizhiqu', '西藏'], + 'CN32' => ['Xinjiang Weiwuerzizhiqu', 'Xinjiang Weiwuerzizhiqu', '新疆'], + // [ 'Taiwan', 'Taiwan', '台湾' ], + // [ 'Hong Kong', 'Hong Kong', '香港' ], + ], + // Czech Republic. + 'CZ' => [], + // Germany. + 'DE' => [], + // Denmark. + 'DK' => [], + // Dominican Republic. + 'DO' => [], + // Algeria. + 'DZ' => [], + // Estonia. + 'EE' => [], + // Egypt. + 'EG' => [ + 'EGALX' => ['Alexandria Governorate', 'Alexandria Governorate', 'الإسكندرية'], + 'EGASN' => ['Aswan Governorate', 'Aswan Governorate', 'أسوان'], + 'EGAST' => ['Asyut Governorate', 'Asyut Governorate', 'أسيوط'], + 'EGBA' => ['Red Sea Governorate', 'Red Sea Governorate', 'البحر الأحمر'], + 'EGBH' => ['El Beheira Governorate', 'El Beheira Governorate', 'البحيرة'], + 'EGBNS' => ['Beni Suef Governorate', 'Beni Suef Governorate', 'بني سويف'], + 'EGC' => ['Cairo Governorate', 'Cairo Governorate', 'القاهرة'], + 'EGDK' => ['Dakahlia Governorate', 'Dakahlia Governorate', 'الدقهلية'], + 'EGDT' => ['Damietta Governorate', 'Damietta Governorate', 'دمياط'], + 'EGFYM' => ['Faiyum Governorate', 'Faiyum Governorate', 'الفيوم'], + 'EGGH' => ['Gharbia Governorate', 'Gharbia Governorate', 'الغربية'], + 'EGGZ' => ['Giza Governorate', 'Giza Governorate', 'الجيزة'], + 'EGIS' => ['Ismailia Governorate', 'Ismailia Governorate', 'الإسماعيلية'], + 'EGJS' => ['South Sinai Governorate', 'South Sinai Governorate', 'جنوب سيناء'], + 'EGKB' => ['Qalyubia Governorate', 'Qalyubia Governorate', 'القليوبية'], + 'EGKFS' => ['Kafr El Sheikh Governorate', 'Kafr El Sheikh Governorate', 'كفر الشيخ'], + 'EGKN' => ['Qena Governorate', 'Qena Governorate', 'قنا'], + 'EGLX' => ['Luxor Governorate', 'Luxor Governorate', 'الأقصر'], + 'EGMN' => ['Menia Governorate', 'Menia Governorate', 'المنيا'], + 'EGMNF' => ['Menofia Governorate', 'Menofia Governorate', 'المنوفية'], + 'EGMT' => ['Matrouh Governorate', 'Matrouh Governorate', 'مطروح'], + 'EGPTS' => ['Port Said Governorate', 'Port Said Governorate', 'بورسعيد'], + 'EGSHG' => ['Sohag Governorate', 'Sohag Governorate', 'سوهاج'], + 'EGSHR' => ['Ash Sharqia Governorate', 'Ash Sharqia Governorate', 'الشرقية'], + 'EGSIN' => ['North Sinai Governorate', 'North Sinai Governorate', 'شمال سيناء'], + 'EGSUZ' => ['Suez Governorate', 'Suez Governorate', 'السويس'], + 'EGWAD' => ['New Valley Governorate', 'New Valley Governorate', 'الوادي الجديد'], + ], + // Spain. + 'ES' => [ + 'C' => ['A Coruña', 'A Coruña', NULL], + 'VI' => ['Álava', 'Álava', NULL], + 'AB' => ['Albacete', 'Albacete', NULL], + 'A' => ['Alicante', 'Alicante', NULL], + 'AL' => ['Almería', 'Almería', NULL], + 'O' => ['Asturias', 'Asturias', NULL], + 'AV' => ['Ávila', 'Ávila', NULL], + 'BA' => ['Badajoz', 'Badajoz', NULL], + 'PM' => ['Balears', 'Balears', NULL], + 'B' => ['Barcelona', 'Barcelona', NULL], + 'BU' => ['Burgos', 'Burgos', NULL], + 'CC' => ['Cáceres', 'Cáceres', NULL], + 'CA' => ['Cádiz', 'Cádiz', NULL], + 'S' => ['Cantabria', 'Cantabria', NULL], + 'CS' => ['Castellón', 'Castellón', NULL], + 'CE' => ['Ceuta', 'Ceuta', NULL], + 'CR' => ['Ciudad Real', 'Ciudad Real', NULL], + 'CO' => ['Córdoba', 'Córdoba', NULL], + 'CU' => ['Cuenca', 'Cuenca', NULL], + 'GI' => ['Girona', 'Girona', NULL], + 'GR' => ['Granada', 'Granada', NULL], + 'GU' => ['Guadalajara', 'Guadalajara', NULL], + 'SS' => ['Guipúzcoa', 'Guipúzcoa', NULL], + 'H' => ['Huelva', 'Huelva', NULL], + 'HU' => ['Huesca', 'Huesca', NULL], + 'J' => ['Jaén', 'Jaén', NULL], + 'LO' => ['La Rioja', 'La Rioja', NULL], + 'GC' => ['Las Palmas', 'Las Palmas', NULL], + 'LE' => ['León', 'León', NULL], + 'L' => ['Lleida', 'Lleida', NULL], + 'LU' => ['Lugo', 'Lugo', NULL], + 'M' => ['Madrid', 'Madrid', NULL], + 'MA' => ['Málaga', 'Málaga', NULL], + 'ML' => ['Melilla', 'Melilla', NULL], + 'MU' => ['Murcia', 'Murcia', NULL], + 'NA' => ['Navarra', 'Navarra', NULL], + 'OR' => ['Ourense', 'Ourense', NULL], + 'P' => ['Palencia', 'Palencia', NULL], + 'PO' => ['Pontevedra', 'Pontevedra', NULL], + 'SA' => ['Salamanca', 'Salamanca', NULL], + 'TF' => ['Santa Cruz de Tenerife', 'Santa Cruz de Tenerife', NULL], + 'SG' => ['Segovia', 'Segovia', NULL], + 'SE' => ['Sevilla', 'Sevilla', NULL], + 'SO' => ['Soria', 'Soria', NULL], + 'T' => ['Tarragona', 'Tarragona', NULL], + 'TE' => ['Teruel', 'Teruel', NULL], + 'TO' => ['Toledo', 'Toledo', NULL], + 'V' => ['Valencia', 'Valencia', NULL], + 'VA' => ['Valladolid', 'Valladolid', NULL], + 'BI' => ['Vizcaya', 'Vizcaya', NULL], + 'ZA' => ['Zamora', 'Zamora', NULL], + 'Z' => ['Zaragoza', 'Zaragoza', NULL], + ], + // Finland. + 'FI' => [], + // France. + 'FR' => [], + // French Guiana. + 'GF' => [], + // Ghana. + 'GH' => [], + // Guadeloupe. + 'GP' => [], + // Greece. + 'GR' => [], + // Guatemala. + 'GT' => [], + // Hong Kong. + 'HK' => [ + 'HONG KONG' => ['Hong Kong Island', 'Hong Kong Island', '香港島'], + 'KOWLOON' => ['Kowloon', 'Kowloon', '九龍'], + 'NEW TERRITORIES' => ['New Territories', 'New Territories', '新界'], + ], + // Hungary. + 'HU' => [], + // Indonesia. + 'ID' => [ + 'AC' => ['Aceh', 'Aceh', NULL], + 'SU' => ['Sumatera Utara', 'Sumatera Utara', NULL], + 'SB' => ['Sumatera Barat', 'Sumatera Barat', NULL], + 'RI' => ['Riau', 'Riau', NULL], + 'KR' => ['Kepulauan Riau', 'Kepulauan Riau', NULL], + 'JA' => ['Jambi', 'Jambi', NULL], + 'SS' => ['Sumatera Selatan', 'Sumatera Selatan', NULL], + 'BB' => ['Kepulauan Bangka Belitung', 'Kepulauan Bangka Belitung', NULL], + 'BE' => ['Bengkulu', 'Bengkulu', NULL], + 'LA' => ['Lampung', 'Lampung', NULL], + 'JK' => ['DKI Jakarta', 'DKI Jakarta', NULL], + 'JB' => ['Jawa Barat', 'Jawa Barat', NULL], + 'BT' => ['Banten', 'Banten', NULL], + 'JT' => ['Jawa Tengah', 'Jawa Tengah', NULL], + 'JI' => ['Jawa Timur', 'Jawa Timur', NULL], + 'YO' => ['Daerah Istimewa Yogyakarta', 'Daerah Istimewa Yogyakarta', NULL], + 'BA' => ['Bali', 'Bali', NULL], + 'NB' => ['Nusa Tenggara Barat', 'Nusa Tenggara Barat', NULL], + 'NT' => ['Nusa Tenggara Timur', 'Nusa Tenggara Timur', NULL], + 'KB' => ['Kalimantan Barat', 'Kalimantan Barat', NULL], + 'KT' => ['Kalimantan Tengah', 'Kalimantan Tengah', NULL], + 'KI' => ['Kalimantan Timur', 'Kalimantan Timur', NULL], + 'KS' => ['Kalimantan Selatan', 'Kalimantan Selatan', NULL], + 'KU' => ['Kalimantan Utara', 'Kalimantan Utara', NULL], + 'SA' => ['Sulawesi Utara', 'Sulawesi Utara', NULL], + 'ST' => ['Sulawesi Tengah', 'Sulawesi Tengah', NULL], + 'SG' => ['Sulawesi Tenggara', 'Sulawesi Tenggara', NULL], + 'SR' => ['Sulawesi Barat', 'Sulawesi Barat', NULL], + 'SN' => ['Sulawesi Selatan', 'Sulawesi Selatan', NULL], + 'GO' => ['Gorontalo', 'Gorontalo', NULL], + 'MA' => ['Maluku', 'Maluku', NULL], + 'MU' => ['Maluku Utara', 'Maluku Utara', NULL], + 'PA' => ['Papua', 'Papua', NULL], + 'PB' => ['Papua Barat', 'Papua Barat', NULL], + // [ 'Kalimantan Tengah', 'Kalimantan Tengah', NULL ], + // [ 'Kalimantan Timur', 'Kalimantan Timur', NULL ], + ], + // Ireland. + 'IE' => [ + 'CW' => ['Co. Carlow', 'Co. Carlow', NULL], + 'CN' => ['Co. Cavan', 'Co. Cavan', NULL], + 'CE' => ['Co. Clare', 'Co. Clare', NULL], + 'CO' => ['Co. Cork', 'Co. Cork', NULL], + 'DL' => ['Co. Donegal', 'Co. Donegal', NULL], + 'D' => ['Co. Dublin', 'Co. Dublin', NULL], + 'G' => ['Co. Galway', 'Co. Galway', NULL], + 'KY' => ['Co. Kerry', 'Co. Kerry', NULL], + 'KE' => ['Co. Kildare', 'Co. Kildare', NULL], + 'KK' => ['Co. Kilkenny', 'Co. Kilkenny', NULL], + 'LS' => ['Co. Laois', 'Co. Laois', NULL], + 'LM' => ['Co. Leitrim', 'Co. Leitrim', NULL], + 'LK' => ['Co. Limerick', 'Co. Limerick', NULL], + 'LD' => ['Co. Longford', 'Co. Longford', NULL], + 'LH' => ['Co. Louth', 'Co. Louth', NULL], + 'MO' => ['Co. Mayo', 'Co. Mayo', NULL], + 'MH' => ['Co. Meath', 'Co. Meath', NULL], + 'MN' => ['Co. Monaghan', 'Co. Monaghan', NULL], + 'OY' => ['Co. Offaly', 'Co. Offaly', NULL], + 'RN' => ['Co. Roscommon', 'Co. Roscommon', NULL], + 'SO' => ['Co. Sligo', 'Co. Sligo', NULL], + 'TA' => ['Co. Tipperary', 'Co. Tipperary', NULL], + 'WD' => ['Co. Waterford', 'Co. Waterford', NULL], + 'WH' => ['Co. Westmeath', 'Co. Westmeath', NULL], + 'WX' => ['Co. Wexford', 'Co. Wexford', NULL], + 'WW' => ['Co. Wicklow', 'Co. Wicklow', NULL], + ], + // Israel. + 'IL' => [], + // Isle of Man. + 'IM' => [], + // India. + 'IN' => [ + 'AP' => ['Andhra Pradesh', 'Andhra Pradesh', NULL], + 'AR' => ['Arunachal Pradesh', 'Arunachal Pradesh', NULL], + 'AS' => ['Assam', 'Assam', NULL], + 'BR' => ['Bihar', 'Bihar', NULL], + 'CT' => ['Chhattisgarh', 'Chhattisgarh', NULL], + 'GA' => ['Goa', 'Goa', NULL], + 'GJ' => ['Gujarat', 'Gujarat', NULL], + 'HR' => ['Haryana', 'Haryana', NULL], + 'HP' => ['Himachal Pradesh', 'Himachal Pradesh', NULL], + 'JK' => ['Jammu and Kashmir', 'Jammu & Kashmir', NULL], + 'JH' => ['Jharkhand', 'Jharkhand', NULL], + 'KA' => ['Karnataka', 'Karnataka', NULL], + 'KL' => ['Kerala', 'Kerala', NULL], + // 'LA' => __( 'Ladakh', 'woocommerce' ), + 'MP' => ['Madhya Pradesh', 'Madhya Pradesh', NULL], + 'MH' => ['Maharashtra', 'Maharashtra', NULL], + 'MN' => ['Manipur', 'Manipur', NULL], + 'ML' => ['Meghalaya', 'Meghalaya', NULL], + 'MZ' => ['Mizoram', 'Mizoram', NULL], + 'NL' => ['Nagaland', 'Nagaland', NULL], + 'OR' => ['Odisha', 'Odisha', NULL], + 'PB' => ['Punjab', 'Punjab', NULL], + 'RJ' => ['Rajasthan', 'Rajasthan', NULL], + 'SK' => ['Sikkim', 'Sikkim', NULL], + 'TN' => ['Tamil Nadu', 'Tamil Nadu', NULL], + 'TS' => ['Telangana', 'Telangana', NULL], + 'TR' => ['Tripura', 'Tripura', NULL], + 'UK' => ['Uttarakhand', 'Uttarakhand', NULL], + 'UP' => ['Uttar Pradesh', 'Uttar Pradesh', NULL], + 'WB' => ['West Bengal', 'West Bengal', NULL], + 'AN' => ['Andaman and Nicobar Islands', 'Andaman & Nicobar', NULL], + 'CH' => ['Chandigarh', 'Chandigarh', NULL], + 'DN' => ['Dadra and Nagar Haveli', 'Dadra & Nagar Haveli', NULL], + 'DD' => ['Daman and Diu', 'Daman & Diu', NULL], + 'DL' => ['Delhi', 'Delhi', NULL], + 'LD' => ['Lakshadweep', 'Lakshadweep', NULL], + 'PY' => ['Puducherry', 'Puducherry', NULL], + ], + // Iran. + 'IR' => [ + 'KHZ' => ['Khuzestan Province', 'Khuzestan Province', 'استان خوزستان'], + 'THR' => ['Tehran Province', 'Tehran Province', 'استان تهران'], + 'ILM' => ['Ilam Province', 'Ilam Province', 'استان ایلام'], + 'BHR' => ['Bushehr Province', 'Bushehr Province', 'استان بوشهر'], + 'ADL' => ['Ardabil Province', 'Ardabil Province', 'استان اردبیل'], + 'ESF' => ['Isfahan Province', 'Isfahan Province', 'استان اصفهان'], + 'YZD' => ['Yazd Province', 'Yazd Province', 'استان یزد'], + 'KRH' => ['Kermanshah Province', 'Kermanshah Province', 'استان کرمانشاه'], + 'KRN' => ['Kerman Province', 'Kerman Province', 'استان کرمان'], + 'HDN' => ['Hamadan Province', 'Hamadan Province', 'استان همدان'], + 'GZN' => ['Qazvin Province', 'Qazvin Province', 'استان قزوین'], + 'ZJN' => ['Zanjan Province', 'Zanjan Province', 'استان زنجان'], + 'LRS' => ['Lorestan Province', 'Lorestan Province', 'استان لرستان'], + 'ABZ' => ['Alborz Province', 'Alborz Province', 'استان البرز'], + 'EAZ' => ['East Azerbaijan Province', 'East Azerbaijan Province', 'استان آذربایجان شرقی'], + 'WAZ' => ['West Azerbaijan Province', 'West Azerbaijan Province', 'استان آذربایجان غربی'], + 'CHB' => ['Chaharmahal and Bakhtiari Province', 'Chaharmahal and Bakhtiari Province', 'استان چهارمحال و بختیاری'], + 'SKH' => ['South Khorasan Province', 'South Khorasan Province', 'استان خراسان جنوبی'], + 'RKH' => ['Razavi Khorasan Province', 'Razavi Khorasan Province', 'استان خراسان رضوی'], + 'NKH' => ['North Khorasan Province', 'North Khorasan Province', 'استان خراسان شمالی'], + 'SMN' => ['Semnan Province', 'Semnan Province', 'استان سمنان'], + 'FRS' => ['Fars Province', 'Fars Province', 'استان فارس'], + 'QHM' => ['Qom Province', 'Qom Province', 'استان قم'], + 'KRD' => ['Kurdistan Province', 'Kurdistan Province', 'استان کردستان'], + 'KBD' => ['Kohgiluyeh and Boyer-Ahmad Province', 'Kohgiluyeh and Boyer-Ahmad Province', 'استان کهگیلویه و بویراحمد'], + 'GLS' => ['Golestan Province', 'Golestan Province', 'استان گلستان'], + 'GIL' => ['Gilan Province', 'Gilan Province', 'استان گیلان'], + 'MZN' => ['Mazandaran Province', 'Mazandaran Province', 'استان مازندران'], + 'MKZ' => ['Markazi Province', 'Markazi Province', 'استان مرکزی'], + 'HRZ' => ['Hormozgan Province', 'Hormozgan Province', 'استان هرمزگان'], + 'SBN' => ['Sistan and Baluchestan Province', 'Sistan and Baluchestan Province', 'استان سیستان و بلوچستان'], + ], + // Iceland. + 'IS' => [], + // Italy. + 'IT' => [ + 'AG' => ['AG', 'Agrigento', NULL], + 'AL' => ['AL', 'Alessandria', NULL], + 'AN' => ['AN', 'Ancona', NULL], + 'AO' => ['AO', 'Aosta', NULL], + 'AR' => ['AR', 'Arezzo', NULL], + 'AP' => ['AP', 'Ascoli Piceno', NULL], + 'AT' => ['AT', 'Asti', NULL], + 'AV' => ['AV', 'Avellino', NULL], + 'BA' => ['BA', 'Bari', NULL], + 'BT' => ['BT', 'Barletta-Andria-Trani', NULL], + 'BL' => ['BL', 'Belluno', NULL], + 'BN' => ['BN', 'Benevento', NULL], + 'BG' => ['BG', 'Bergamo', NULL], + 'BI' => ['BI', 'Biella', NULL], + 'BO' => ['BO', 'Bologna', NULL], + 'BZ' => ['BZ', 'Bolzano', NULL], + 'BS' => ['BS', 'Brescia', NULL], + 'BR' => ['BR', 'Brindisi', NULL], + 'CA' => ['CA', 'Cagliari', NULL], + 'CL' => ['CL', 'Caltanissetta', NULL], + 'CB' => ['CB', 'Campobasso', NULL], + 'CE' => ['CE', 'Caserta', NULL], + 'CT' => ['CT', 'Catania', NULL], + 'CZ' => ['CZ', 'Catanzaro', NULL], + 'CH' => ['CH', 'Chieti', NULL], + 'CO' => ['CO', 'Como', NULL], + 'CS' => ['CS', 'Cosenza', NULL], + 'CR' => ['CR', 'Cremona', NULL], + 'KR' => ['KR', 'Crotone', NULL], + 'CN' => ['CN', 'Cuneo', NULL], + 'EN' => ['EN', 'Enna', NULL], + 'FM' => ['FM', 'Fermo', NULL], + 'FE' => ['FE', 'Ferrara', NULL], + 'FI' => ['FI', 'Firenze', NULL], + 'FG' => ['FG', 'Foggia', NULL], + 'FC' => ['FC', 'Forlì-Cesena', NULL], + 'FR' => ['FR', 'Frosinone', NULL], + 'GE' => ['GE', 'Genova', NULL], + 'GO' => ['GO', 'Gorizia', NULL], + 'GR' => ['GR', 'Grosseto', NULL], + 'IM' => ['IM', 'Imperia', NULL], + 'IS' => ['IS', 'Isernia', NULL], + 'SP' => ['SP', 'La Spezia', NULL], + 'AQ' => ['AQ', "L'Aquila", NULL], + 'LT' => ['LT', 'Latina', NULL], + 'LE' => ['LE', 'Lecce', NULL], + 'LC' => ['LC', 'Lecco', NULL], + 'LI' => ['LI', 'Livorno', NULL], + 'LO' => ['LO', 'Lodi', NULL], + 'LU' => ['LU', 'Lucca', NULL], + 'MC' => ['MC', 'Macerata', NULL], + 'MN' => ['MN', 'Mantova', NULL], + 'MS' => ['MS', 'Massa-Carrara', NULL], + 'MT' => ['MT', 'Matera', NULL], + 'ME' => ['ME', 'Messina', NULL], + 'MI' => ['MI', 'Milano', NULL], + 'MO' => ['MO', 'Modena', NULL], + 'MB' => ['MB', 'Monza e Brianza', NULL], + 'NA' => ['NA', 'Napoli', NULL], + 'NO' => ['NO', 'Novara', NULL], + 'NU' => ['NU', 'Nuoro', NULL], + 'OR' => ['OR', 'Oristano', NULL], + 'PD' => ['PD', 'Padova', NULL], + 'PA' => ['PA', 'Palermo', NULL], + 'PR' => ['PR', 'Parma', NULL], + 'PV' => ['PV', 'Pavia', NULL], + 'PG' => ['PG', 'Perugia', NULL], + 'PU' => ['PU', 'Pesaro e Urbino', NULL], + 'PE' => ['PE', 'Pescara', NULL], + 'PC' => ['PC', 'Piacenza', NULL], + 'PI' => ['PI', 'Pisa', NULL], + 'PT' => ['PT', 'Pistoia', NULL], + 'PN' => ['PN', 'Pordenone', NULL], + 'PZ' => ['PZ', 'Potenza', NULL], + 'PO' => ['PO', 'Prato', NULL], + 'RG' => ['RG', 'Ragusa', NULL], + 'RA' => ['RA', 'Ravenna', NULL], + 'RC' => ['RC', 'Reggio Calabria', NULL], + 'RE' => ['RE', 'Reggio Emilia', NULL], + 'RI' => ['RI', 'Rieti', NULL], + 'RN' => ['RN', 'Rimini', NULL], + 'RM' => ['RM', 'Roma', NULL], + 'RO' => ['RO', 'Rovigo', NULL], + 'SA' => ['SA', 'Salerno', NULL], + 'SS' => ['SS', 'Sassari', NULL], + 'SV' => ['SV', 'Savona', NULL], + 'SI' => ['SI', 'Siena', NULL], + 'SR' => ['SR', 'Siracusa', NULL], + 'SO' => ['SO', 'Sondrio', NULL], + 'SU' => ['SU', 'Sud Sardegna', NULL], + 'TA' => ['TA', 'Taranto', NULL], + 'TE' => ['TE', 'Teramo', NULL], + 'TR' => ['TR', 'Terni', NULL], + 'TO' => ['TO', 'Torino', NULL], + 'TP' => ['TP', 'Trapani', NULL], + 'TN' => ['TN', 'Trento', NULL], + 'TV' => ['TV', 'Treviso', NULL], + 'TS' => ['TS', 'Trieste', NULL], + 'UD' => ['UD', 'Udine', NULL], + 'VA' => ['VA', 'Varese', NULL], + 'VE' => ['VE', 'Venezia', NULL], + 'VB' => ['VB', 'Verbano-Cusio-Ossola', NULL], + 'VC' => ['VC', 'Vercelli', NULL], + 'VR' => ['VR', 'Verona', NULL], + 'VV' => ['VV', 'Vibo Valentia', NULL], + 'VI' => ['VI', 'Vicenza', NULL], + 'VT' => ['VT', 'Viterbo', NULL], + ], + // Jamaica. + 'JM' => [ + 'JM-01' => ['Kingston', 'Kingston', NULL], + 'JM-02' => ['St. Andrew', 'St. Andrew', NULL], + 'JM-03' => ['St. Thomas', 'St. Thomas', NULL], + 'JM-04' => ['Portland', 'Portland', NULL], + 'JM-05' => ['St. Mary', 'St. Mary', NULL], + 'JM-06' => ['St. Ann', 'St. Ann', NULL], + 'JM-07' => ['Trelawny', 'Trelawny', NULL], + 'JM-08' => ['St. James', 'St. James', NULL], + 'JM-09' => ['Hanover', 'Hanover', NULL], + 'JM-10' => ['Westmoreland', 'Westmoreland', NULL], + 'JM-11' => ['St. Elizabeth', 'St. Elizabeth', NULL], + 'JM-12' => ['Manchester', 'Manchester', NULL], + 'JM-13' => ['Clarendon', 'Clarendon', NULL], + 'JM-14' => ['St. Catherine', 'St. Catherine', NULL], + ], + // Japan. + 'JP' => [ + 'JP01' => ['Hokkaido', 'Hokkaido', '北海道'], + 'JP02' => ['Aomori', 'Aomori', '青森県'], + 'JP03' => ['Iwate', 'Iwate', '岩手県'], + 'JP04' => ['Miyagi', 'Miyagi', '宮城県'], + 'JP05' => ['Akita', 'Akita', '秋田県'], + 'JP06' => ['Yamagata', 'Yamagata', '山形県'], + 'JP07' => ['Fukushima', 'Fukushima', '福島県'], + 'JP08' => ['Ibaraki', 'Ibaraki', '茨城県'], + 'JP09' => ['Tochigi', 'Tochigi', '栃木県'], + 'JP10' => ['Gunma', 'Gunma', '群馬県'], + 'JP11' => ['Saitama', 'Saitama', '埼玉県'], + 'JP12' => ['Chiba', 'Chiba', '千葉県'], + 'JP13' => ['Tokyo', 'Tokyo', '東京都'], + 'JP14' => ['Kanagawa', 'Kanagawa', '神奈川県'], + 'JP15' => ['Niigata', 'Niigata', '新潟県'], + 'JP16' => ['Toyama', 'Toyama', '富山県'], + 'JP17' => ['Ishikawa', 'Ishikawa', '石川県'], + 'JP18' => ['Fukui', 'Fukui', '福井県'], + 'JP19' => ['Yamanashi', 'Yamanashi', '山梨県'], + 'JP20' => ['Nagano', 'Nagano', '長野県'], + 'JP21' => ['Gifu', 'Gifu', '岐阜県'], + 'JP22' => ['Shizuoka', 'Shizuoka', '静岡県'], + 'JP23' => ['Aichi', 'Aichi', '愛知県'], + 'JP24' => ['Mie', 'Mie', '三重県'], + 'JP25' => ['Shiga', 'Shiga', '滋賀県'], + 'JP26' => ['Kyoto', 'Kyoto', '京都府'], + 'JP27' => ['Osaka', 'Osaka', '大阪府'], + 'JP28' => ['Hyogo', 'Hyogo', '兵庫県'], + 'JP29' => ['Nara', 'Nara', '奈良県'], + 'JP30' => ['Wakayama', 'Wakayama', '和歌山県'], + 'JP31' => ['Tottori', 'Tottori', '鳥取県'], + 'JP32' => ['Shimane', 'Shimane', '島根県'], + 'JP33' => ['Okayama', 'Okayama', '岡山県'], + 'JP34' => ['Hiroshima', 'Hiroshima', '広島県'], + 'JP35' => ['Yamaguchi', 'Yamaguchi', '山口県'], + 'JP36' => ['Tokushima', 'Tokushima', '徳島県'], + 'JP37' => ['Kagawa', 'Kagawa', '香川県'], + 'JP38' => ['Ehime', 'Ehime', '愛媛県'], + 'JP39' => ['Kochi', 'Kochi', '高知県'], + 'JP40' => ['Fukuoka', 'Fukuoka', '福岡県'], + 'JP41' => ['Saga', 'Saga', '佐賀県'], + 'JP42' => ['Nagasaki', 'Nagasaki', '長崎県'], + 'JP43' => ['Kumamoto', 'Kumamoto', '熊本県'], + 'JP44' => ['Oita', 'Oita', '大分県'], + 'JP45' => ['Miyazaki', 'Miyazaki', '宮崎県'], + 'JP46' => ['Kagoshima', 'Kagoshima', '鹿児島県'], + 'JP47' => ['Okinawa', 'Okinawa', '沖縄県'], + ], + // Kenya. + 'KE' => [], + // South Korea. + 'KR' => [], + // Kuwait. + 'KW' => [], + // Laos. + 'LA' => [], + // Lebanon. + 'LB' => [], + // Sri Lanka. + 'LK' => [], + // Liberia. + 'LR' => [], + // Luxembourg. + 'LU' => [], + // Moldova. + 'MD' => [], + // Martinique. + 'MQ' => [], + // Malta. + 'MT' => [], + // Mexico. + 'MX' => [ + 'DF' => ['CDMX', 'Ciudad de México', NULL], + 'JA' => ['Jal.', 'Jalisco', NULL], + 'NL' => ['N.L.', 'Nuevo León', NULL], + 'AG' => ['Ags.', 'Aguascalientes', NULL], + 'BC' => ['B.C.', 'Baja California', NULL], + 'BS' => ['B.C.S.', 'Baja California Sur', NULL], + 'CM' => ['Camp.', 'Campeche', NULL], + 'CS' => ['Chis.', 'Chiapas', NULL], + 'CH' => ['Chih.', 'Chihuahua', NULL], + 'CO' => ['Coah.', 'Coahuila de Zaragoza', NULL], + 'CL' => ['Col.', 'Colima', NULL], + 'DG' => ['Dgo.', 'Durango', NULL], + 'GT' => ['Gto.', 'Guanajuato', NULL], + 'GR' => ['Gro.', 'Guerrero', NULL], + 'HG' => ['Hgo.', 'Hidalgo', NULL], + 'MX' => ['Méx.', 'Estado de México', NULL], + 'MI' => ['Mich.', 'Michoacán', NULL], + 'MO' => ['Mor.', 'Morelos', NULL], + 'NA' => ['Nay.', 'Nayarit', NULL], + 'OA' => ['Oax.', 'Oaxaca', NULL], + 'PU' => ['Pue.', 'Puebla', NULL], + 'QT' => ['Qro.', 'Querétaro', NULL], + 'QR' => ['Q.R.', 'Quintana Roo', NULL], + 'SL' => ['S.L.P.', 'San Luis Potosí', NULL], + 'SI' => ['Sin.', 'Sinaloa', NULL], + 'SO' => ['Son.', 'Sonora', NULL], + 'TB' => ['Tab.', 'Tabasco', NULL], + 'TM' => ['Tamps.', 'Tamaulipas', NULL], + 'TL' => ['Tlax.', 'Tlaxcala', NULL], + 'VE' => ['Ver.', 'Veracruz', NULL], + 'YU' => ['Yuc.', 'Yucatán', NULL], + 'ZA' => ['Zac.', 'Zacatecas', NULL], + ], + // Malaysia. + 'MY' => [ + 'JHR' => ['Johor', 'Johor', NULL], + 'KDH' => ['Kedah', 'Kedah', NULL], + 'KTN' => ['Kelantan', 'Kelantan', NULL], + 'LBN' => ['Labuan', 'Labuan', NULL], + 'MLK' => ['Melaka', 'Melaka', NULL], + 'NSN' => ['Negeri Sembilan', 'Negeri Sembilan', NULL], + 'PHG' => ['Pahang', 'Pahang', NULL], + 'PNG' => ['Pulau Pinang', 'Pulau Pinang', NULL], + 'PRK' => ['Perak', 'Perak', NULL], + 'PLS' => ['Perlis', 'Perlis', NULL], + 'SBH' => ['Sabah', 'Sabah', NULL], + 'SWK' => ['Sarawak', 'Sarawak', NULL], + 'SGR' => ['Selangor', 'Selangor', NULL], + 'TRG' => ['Terengganu', 'Terengganu', NULL], + 'PJY' => ['Putrajaya', 'Putrajaya', NULL], + 'KUL' => ['Kuala Lumpur', 'Kuala Lumpur', NULL], + ], + // Mozambique. + 'MZ' => [ + 'MZP' => ['Cabo Delgado', 'Cabo Delgado', NULL], + 'MZG' => ['Gaza', 'Gaza', NULL], + 'MZI' => ['Inhambane', 'Inhambane', NULL], + 'MZB' => ['Manica', 'Manica', NULL], + 'MZL' => ['Maputo', 'Maputo', NULL], + 'MZMPM' => ['Cidade de Maputo', 'Cidade de Maputo', NULL], + 'MZN' => ['Nampula', 'Nampula', NULL], + 'MZA' => ['Niassa', 'Niassa', NULL], + 'MZS' => ['Sofala', 'Sofala', NULL], + 'MZT' => ['Tete', 'Tete', NULL], + 'MZQ' => ['Zambezia', 'Zambezia', NULL], + ], + // Namibia. + 'NA' => [], + // Nigeria. + 'NG' => [ + 'AB' => ['Abia', 'Abia', NULL], + 'FC' => ['Federal Capital Territory', 'Federal Capital Territory', NULL], + 'AD' => ['Adamawa', 'Adamawa', NULL], + 'AK' => ['Akwa Ibom', 'Akwa Ibom', NULL], + 'AN' => ['Anambra', 'Anambra', NULL], + 'BA' => ['Bauchi', 'Bauchi', NULL], + 'BY' => ['Bayelsa', 'Bayelsa', NULL], + 'BE' => ['Benue', 'Benue', NULL], + 'BO' => ['Borno', 'Borno', NULL], + 'CR' => ['Cross River', 'Cross River', NULL], + 'DE' => ['Delta', 'Delta', NULL], + 'EB' => ['Ebonyi', 'Ebonyi', NULL], + 'ED' => ['Edo', 'Edo', NULL], + 'EK' => ['Ekiti', 'Ekiti', NULL], + 'EN' => ['Enugu', 'Enugu', NULL], + 'GO' => ['Gombe', 'Gombe', NULL], + 'IM' => ['Imo', 'Imo', NULL], + 'JI' => ['Jigawa', 'Jigawa', NULL], + 'KD' => ['Kaduna', 'Kaduna', NULL], + 'KN' => ['Kano', 'Kano', NULL], + 'KT' => ['Katsina', 'Katsina', NULL], + 'KE' => ['Kebbi', 'Kebbi', NULL], + 'KO' => ['Kogi', 'Kogi', NULL], + 'KW' => ['Kwara', 'Kwara', NULL], + 'LA' => ['Lagos', 'Lagos', NULL], + 'NA' => ['Nasarawa', 'Nasarawa', NULL], + 'NI' => ['Niger', 'Niger', NULL], + 'OG' => ['Ogun State', 'Ogun State', NULL], + 'ON' => ['Ondo', 'Ondo', NULL], + 'OS' => ['Osun', 'Osun', NULL], + 'OY' => ['Oyo', 'Oyo', NULL], + 'PL' => ['Plateau', 'Plateau', NULL], + 'RI' => ['Rivers', 'Rivers', NULL], + 'SO' => ['Sokoto', 'Sokoto', NULL], + 'TA' => ['Taraba', 'Taraba', NULL], + 'YO' => ['Yobe', 'Yobe', NULL], + 'ZA' => ['Zamfara', 'Zamfara', NULL], + ], + // Netherlands. + 'NL' => [], + // Norway. + 'NO' => [], + // Nepal. + 'NP' => [], + // New Zealand. + 'NZ' => [], + // Peru. + 'PE' => [ + 'CAL' => ['Callao', 'Callao', NULL], + 'LMA' => ['Municipalidad Metropolitana de Lima', 'Municipalidad Metropolitana de Lima', NULL], + 'AMA' => ['Amazonas', 'Amazonas', NULL], + 'ANC' => ['Áncash', 'Áncash', NULL], + 'APU' => ['Apurímac', 'Apurímac', NULL], + 'ARE' => ['Arequipa', 'Arequipa', NULL], + 'AYA' => ['Ayacucho', 'Ayacucho', NULL], + 'CAJ' => ['Cajamarca', 'Cajamarca', NULL], + 'CUS' => ['Cuzco', 'Cuzco', NULL], + 'HUV' => ['Huancavelica', 'Huancavelica', NULL], + 'HUC' => ['Huánuco', 'Huánuco', NULL], + 'ICA' => ['Ica', 'Ica', NULL], + 'JUN' => ['Junín', 'Junín', NULL], + 'LAL' => ['La Libertad', 'La Libertad', NULL], + 'LAM' => ['Lambayeque', 'Lambayeque', NULL], + 'LIM' => ['Gobierno Regional de Lima', 'Gobierno Regional de Lima', NULL], + 'LOR' => ['Loreto', 'Loreto', NULL], + 'MDD' => ['Madre de Dios', 'Madre de Dios', NULL], + 'MOQ' => ['Moquegua', 'Moquegua', NULL], + 'PAS' => ['Pasco', 'Pasco', NULL], + 'PIU' => ['Piura', 'Piura', NULL], + 'PUN' => ['Puno', 'Puno', NULL], + 'SAM' => ['San Martín', 'San Martín', NULL], + 'TAC' => ['Tacna', 'Tacna', NULL], + 'TUM' => ['Tumbes', 'Tumbes', NULL], + 'UCA' => ['Ucayali', 'Ucayali', NULL], + ], + // Philippines. + 'PH' => [ + 'ABR' => ['Abra', 'Abra', NULL], + 'AGN' => ['Agusan del Norte', 'Agusan del Norte', NULL], + 'AGS' => ['Agusan del Sur', 'Agusan del Sur', NULL], + 'AKL' => ['Aklan', 'Aklan', NULL], + 'ALB' => ['Albay', 'Albay', NULL], + 'ANT' => ['Antique', 'Antique', NULL], + 'APA' => ['Apayao', 'Apayao', NULL], + 'AUR' => ['Aurora', 'Aurora', NULL], + 'BAS' => ['Basilan', 'Basilan', NULL], + 'BAN' => ['Bataan', 'Bataan', NULL], + 'BTN' => ['Batanes', 'Batanes', NULL], + 'BTG' => ['Batangas', 'Batangas', NULL], + 'BEN' => ['Benguet', 'Benguet', NULL], + 'BIL' => ['Biliran', 'Biliran', NULL], + 'BOH' => ['Bohol', 'Bohol', NULL], + 'BUK' => ['Bukidnon', 'Bukidnon', NULL], + 'BUL' => ['Bulacan', 'Bulacan', NULL], + 'CAG' => ['Cagayan', 'Cagayan', NULL], + 'CAN' => ['Camarines Norte', 'Camarines Norte', NULL], + 'CAS' => ['Camarines Sur', 'Camarines Sur', NULL], + 'CAM' => ['Camiguin', 'Camiguin', NULL], + 'CAP' => ['Capiz', 'Capiz', NULL], + 'CAT' => ['Catanduanes', 'Catanduanes', NULL], + 'CAV' => ['Cavite', 'Cavite', NULL], + 'CEB' => ['Cebu', 'Cebu', NULL], + 'COM' => ['Compostela Valley', 'Compostela Valley', NULL], + 'NCO' => ['Cotabato', 'Cotabato', NULL], + 'DAV' => ['Davao del Norte', 'Davao del Norte', NULL], + 'DAS' => ['Davao del Sur', 'Davao del Sur', NULL], + 'DAC' => ['Davao Occidental', 'Davao Occidental', NULL], + 'DAO' => ['Davao Oriental', 'Davao Oriental', NULL], + 'DIN' => ['Dinagat Islands', 'Dinagat Islands', NULL], + 'EAS' => ['Eastern Samar', 'Eastern Samar', NULL], + 'GUI' => ['Guimaras', 'Guimaras', NULL], + 'IFU' => ['Ifugao', 'Ifugao', NULL], + 'ILN' => ['Ilocos Norte', 'Ilocos Norte', NULL], + 'ILS' => ['Ilocos Sur', 'Ilocos Sur', NULL], + 'ILI' => ['Iloilo', 'Iloilo', NULL], + 'ISA' => ['Isabela', 'Isabela', NULL], + 'KAL' => ['Kalinga', 'Kalinga', NULL], + 'LUN' => ['La Union', 'La Union', NULL], + 'LAG' => ['Laguna', 'Laguna', NULL], + 'LAN' => ['Lanao del Norte', 'Lanao del Norte', NULL], + 'LAS' => ['Lanao del Sur', 'Lanao del Sur', NULL], + 'LEY' => ['Leyte', 'Leyte', NULL], + 'MAG' => ['Maguindanao', 'Maguindanao', NULL], + 'MAD' => ['Marinduque', 'Marinduque', NULL], + 'MAS' => ['Masbate', 'Masbate', NULL], + 'MSC' => ['Misamis Occidental', 'Misamis Occidental', NULL], + 'MSR' => ['Misamis Oriental', 'Misamis Oriental', NULL], + 'MOU' => ['Mountain Province', 'Mountain Province', NULL], + 'NEC' => ['Negros Occidental', 'Negros Occidental', NULL], + 'NER' => ['Negros Oriental', 'Negros Oriental', NULL], + 'NSA' => ['Northern Samar', 'Northern Samar', NULL], + 'NUE' => ['Nueva Ecija', 'Nueva Ecija', NULL], + 'NUV' => ['Nueva Vizcaya', 'Nueva Vizcaya', NULL], + 'MDC' => ['Mindoro Occidental', 'Mindoro Occidental', NULL], + 'MDR' => ['Mindoro Oriental', 'Mindoro Oriental', NULL], + 'PLW' => ['Palawan', 'Palawan', NULL], + 'PAM' => ['Pampanga', 'Pampanga', NULL], + 'PAN' => ['Pangasinan', 'Pangasinan', NULL], + 'QUE' => ['Quezon Province', 'Quezon Province', NULL], + 'QUI' => ['Quirino', 'Quirino', NULL], + 'RIZ' => ['Rizal', 'Rizal', NULL], + 'ROM' => ['Romblon', 'Romblon', NULL], + 'WSA' => ['Samar', 'Samar', NULL], + 'SAR' => ['Sarangani', 'Sarangani', NULL], + 'SIQ' => ['Siquijor', 'Siquijor', NULL], + 'SOR' => ['Sorsogon', 'Sorsogon', NULL], + 'SCO' => ['South Cotabato', 'South Cotabato', NULL], + 'SLE' => ['Southern Leyte', 'Southern Leyte', NULL], + 'SUK' => ['Sultan Kudarat', 'Sultan Kudarat', NULL], + 'SLU' => ['Sulu', 'Sulu', NULL], + 'SUN' => ['Surigao del Norte', 'Surigao del Norte', NULL], + 'SUR' => ['Surigao del Sur', 'Surigao del Sur', NULL], + 'TAR' => ['Tarlac', 'Tarlac', NULL], + 'TAW' => ['Tawi-Tawi', 'Tawi-Tawi', NULL], + 'ZMB' => ['Zambales', 'Zambales', NULL], + 'ZAN' => ['Zamboanga del Norte', 'Zamboanga del Norte', NULL], + 'ZAS' => ['Zamboanga del Sur', 'Zamboanga del Sur', NULL], + 'ZSI' => ['Zamboanga Sibuguey', 'Zamboanga Sibuguey', NULL], + '00' => ['Metro Manila', 'Metro Manila', NULL], + ], + // Pakistan. + 'PK' => [], + // Poland. + 'PL' => [], + // Puerto Rico. + 'PR' => [], + // Portugal. + 'PT' => [], + // Paraguay. + 'PY' => [], + // Reunion. + 'RE' => [], + // Romania. + 'RO' => [], + // Serbia. + 'RS' => [], + // Sweden. + 'SE' => [], + // Singapore. + 'SG' => [], + // Slovenia. + 'SI' => [], + // Slovakia. + 'SK' => [], + // Thailand. + 'TH' => [ + 'TH-37' => ['Amnat Charoen', 'Amnat Charoen', 'อำนาจเจริญ'], + 'TH-15' => ['Ang Thong', 'Ang Thong', 'อ่างทอง'], + 'TH-14' => ['Phra Nakhon Si Ayutthaya', 'Phra Nakhon Si Ayutthaya', 'พระนครศรีอยุธยา'], + 'TH-10' => ['Bangkok', 'Bangkok', 'กรุงเทพมหานคร'], + 'TH-38' => ['Bueng Kan', 'Bueng Kan', 'จังหวัด บึงกาฬ'], + 'TH-31' => ['Buri Ram', 'Buri Ram', 'บุรีรัมย์'], + 'TH-24' => ['Chachoengsao', 'Chachoengsao', 'ฉะเชิงเทรา'], + 'TH-18' => ['Chai Nat', 'Chai Nat', 'ชัยนาท'], + 'TH-36' => ['Chaiyaphum', 'Chaiyaphum', 'ชัยภูมิ'], + 'TH-22' => ['Chanthaburi', 'Chanthaburi', 'จันทบุรี'], + 'TH-50' => ['Chiang Rai', 'Chiang Rai', 'เชียงราย'], + 'TH-57' => ['Chiang Mai', 'Chiang Mai', 'เชียงใหม่'], + 'TH-20' => ['Chon Buri', 'Chon Buri', 'ชลบุรี'], + 'TH-86' => ['Chumpon', 'Chumpon', 'ชุมพร'], + 'TH-46' => ['Kalasin', 'Kalasin', 'กาฬสินธุ์'], + 'TH-62' => ['Kamphaeng Phet', 'Kamphaeng Phet', 'กำแพงเพชร'], + 'TH-71' => ['Kanchanaburi', 'Kanchanaburi', 'กาญจนบุรี'], + 'TH-40' => ['Khon Kaen', 'Khon Kaen', 'ขอนแก่น'], + 'TH-81' => ['Krabi', 'Krabi', 'กระบี่'], + 'TH-52' => ['Lampang', 'Lampang', 'ลำปาง'], + 'TH-51' => ['Lamphun', 'Lamphun', 'ลำพูน'], + 'TH-42' => ['Loei', 'Loei', 'เลย'], + 'TH-16' => ['Lop Buri', 'Lop Buri', 'ลพบุรี'], + 'TH-58' => ['Mae Hong Son', 'Mae Hong Son', 'แม่ฮ่องสอน'], + 'TH-44' => ['Maha Sarakham', 'Maha Sarakham', 'มหาสารคาม'], + 'TH-49' => ['Mukdahan', 'Mukdahan', 'มุกดาหาร'], + 'TH-26' => ['Nakhon Nayok', 'Nakhon Nayok', 'นครนายก'], + 'TH-73' => ['Nakhon Pathom', 'Nakhon Pathom', 'นครปฐม'], + 'TH-48' => ['Nakhon Phanom', 'Nakhon Phanom', 'นครพนม'], + 'TH-30' => ['Nakhon Ratchasima', 'Nakhon Ratchasima', 'นครราชสีมา'], + 'TH-60' => ['Nakhon Sawan', 'Nakhon Sawan', 'นครสวรรค์'], + 'TH-80' => ['Nakhon Si Thammarat', 'Nakhon Si Thammarat', 'นครศรีธรรมราช'], + 'TH-55' => ['Nan', 'Nan', 'น่าน'], + 'TH-96' => ['Narathiwat', 'Narathiwat', 'นราธิวาส'], + 'TH-39' => ['Nong Bua Lam Phu', 'Nong Bua Lam Phu', 'หนองบัวลำภู'], + 'TH-43' => ['Nong Khai', 'Nong Khai', 'หนองคาย'], + 'TH-12' => ['Nonthaburi', 'Nonthaburi', 'นนทบุรี'], + 'TH-13' => ['Pathum Thani', 'Pathum Thani', 'ปทุมธานี'], + 'TH-94' => ['Pattani', 'Pattani', 'ปัตตานี'], + 'TH-82' => ['Phang Nga', 'Phang Nga', 'พังงา'], + 'TH-93' => ['Phattalung', 'Phattalung', 'พัทลุง'], + 'TH-56' => ['Phayao', 'Phayao', 'พะเยา'], + 'TH-67' => ['Phetchabun', 'Phetchabun', 'เพชรบูรณ์'], + 'TH-76' => ['Phetchaburi', 'Phetchaburi', 'เพชรบุรี'], + 'TH-66' => ['Phichit', 'Phichit', 'พิจิตร'], + 'TH-65' => ['Phitsanulok', 'Phitsanulok', 'พิษณุโลก'], + 'TH-54' => ['Phrae', 'Phrae', 'แพร่'], + 'TH-83' => ['Phuket', 'Phuket', 'ภูเก็ต'], + 'TH-25' => ['Prachin Buri', 'Prachin Buri', 'ปราจีนบุรี'], + 'TH-77' => ['Prachuap Khiri Khan', 'Prachuap Khiri Khan', 'ประจวบคีรีขันธ์'], + 'TH-85' => ['Ranong', 'Ranong', 'ระนอง'], + 'TH-70' => ['Ratchaburi', 'Ratchaburi', 'ราชบุรี'], + 'TH-21' => ['Rayong', 'Rayong', 'ระยอง'], + 'TH-45' => ['Roi Et', 'Roi Et', 'ร้อยเอ็ด'], + 'TH-27' => ['Sa Kaeo', 'Sa Kaeo', 'สระแก้ว'], + 'TH-47' => ['Sakon Nakhon', 'Sakon Nakhon', 'สกลนคร'], + 'TH-11' => ['Samut Prakan', 'Samut Prakan', 'สมุทรปราการ'], + 'TH-74' => ['Samut Sakhon', 'Samut Sakhon', 'สมุทรสาคร'], + 'TH-75' => ['Samut Songkhram', 'Samut Songkhram', 'สมุทรสงคราม'], + 'TH-19' => ['Saraburi', 'Saraburi', 'สระบุรี'], + 'TH-91' => ['Satun', 'Satun', 'สตูล'], + 'TH-17' => ['Sing Buri', 'Sing Buri', 'สิงห์บุรี'], + 'TH-33' => ['Si Sa Ket', 'Si Sa Ket', 'ศรีสะเกษ'], + 'TH-90' => ['Songkhla', 'Songkhla', 'สงขลา'], + 'TH-64' => ['Sukhothai', 'Sukhothai', 'สุโขทัย'], + 'TH-72' => ['Suphanburi', 'Suphanburi', 'สุพรรณบุรี'], + 'TH-84' => ['Surat Thani', 'Surat Thani', 'สุราษฎร์ธานี'], + 'TH-32' => ['Surin', 'Surin', 'สุรินทร์'], + 'TH-63' => ['Tak', 'Tak', 'ตาก'], + 'TH-92' => ['Trang', 'Trang', 'ตรัง'], + 'TH-23' => ['Trat', 'Trat', 'ตราด'], + 'TH-34' => ['Ubon Ratchathani', 'Ubon Ratchathani', 'อุบลราชธานี'], + 'TH-41' => ['Udon Thani', 'Udon Thani', 'อุดรธานี'], + 'TH-61' => ['Uthai Thani', 'Uthai Thani', 'อุทัยธานี'], + 'TH-53' => ['Uttaradit', 'Uttaradit', 'อุตรดิตถ์'], + 'TH-95' => ['Yala', 'Yala', 'ยะลา'], + 'TH-35' => ['Yasothon', 'Yasothon', 'ยโสธร'], + ], + // Turkey. + 'TR' => [ + 'TR01' => ['Adana', 'Adana', NULL], + 'TR02' => ['Adıyaman', 'Adıyaman', NULL], + 'TR03' => ['Afyon', 'Afyon', NULL], + 'TR04' => ['Ağrı', 'Ağrı', NULL], + 'TR05' => ['Amasya', 'Amasya', NULL], + 'TR06' => ['Ankara', 'Ankara', NULL], + 'TR07' => ['Antalya', 'Antalya', NULL], + 'TR08' => ['Artvin', 'Artvin', NULL], + 'TR09' => ['Aydın', 'Aydın', NULL], + 'TR10' => ['Balıkesir', 'Balıkesir', NULL], + 'TR11' => ['Bilecik', 'Bilecik', NULL], + 'TR12' => ['Bingöl', 'Bingöl', NULL], + 'TR13' => ['Bitlis', 'Bitlis', NULL], + 'TR14' => ['Bolu', 'Bolu', NULL], + 'TR15' => ['Burdur', 'Burdur', NULL], + 'TR16' => ['Bursa', 'Bursa', NULL], + 'TR17' => ['Çanakkale', 'Çanakkale', NULL], + 'TR18' => ['Çankırı', 'Çankırı', NULL], + 'TR19' => ['Çorum', 'Çorum', NULL], + 'TR20' => ['Denizli', 'Denizli', NULL], + 'TR21' => ['Diyarbakır', 'Diyarbakır', NULL], + 'TR22' => ['Edirne', 'Edirne', NULL], + 'TR23' => ['Elazığ', 'Elazığ', NULL], + 'TR24' => ['Erzincan', 'Erzincan', NULL], + 'TR25' => ['Erzurum', 'Erzurum', NULL], + 'TR26' => ['Eskişehir', 'Eskişehir', NULL], + 'TR27' => ['Gaziantep', 'Gaziantep', NULL], + 'TR28' => ['Giresun', 'Giresun', NULL], + 'TR29' => ['Gümüşhane', 'Gümüşhane', NULL], + 'TR30' => ['Hakkari', 'Hakkari', NULL], + 'TR31' => ['Hatay', 'Hatay', NULL], + 'TR32' => ['Isparta', 'Isparta', NULL], + 'TR33' => ['Mersin', 'Mersin', NULL], + 'TR34' => ['İstanbul', 'İstanbul', NULL], + 'TR35' => ['İzmir', 'İzmir', NULL], + 'TR36' => ['Kars', 'Kars', NULL], + 'TR37' => ['Kastamonu', 'Kastamonu', NULL], + 'TR38' => ['Kayseri', 'Kayseri', NULL], + 'TR39' => ['Kırklareli', 'Kırklareli', NULL], + 'TR40' => ['Kırşehir', 'Kırşehir', NULL], + 'TR41' => ['Kocaeli', 'Kocaeli', NULL], + 'TR42' => ['Konya', 'Konya', NULL], + 'TR43' => ['Kütahya', 'Kütahya', NULL], + 'TR44' => ['Malatya', 'Malatya', NULL], + 'TR45' => ['Manisa', 'Manisa', NULL], + 'TR46' => ['Kahramanmaraş', 'Kahramanmaraş', NULL], + 'TR47' => ['Mardin', 'Mardin', NULL], + 'TR48' => ['Muğla', 'Muğla', NULL], + 'TR49' => ['Muş', 'Muş', NULL], + 'TR50' => ['Nevşehir', 'Nevşehir', NULL], + 'TR51' => ['Niğde', 'Niğde', NULL], + 'TR52' => ['Ordu', 'Ordu', NULL], + 'TR53' => ['Rize', 'Rize', NULL], + 'TR54' => ['Sakarya', 'Sakarya', NULL], + 'TR55' => ['Samsun', 'Samsun', NULL], + 'TR56' => ['Siirt', 'Siirt', NULL], + 'TR57' => ['Sinop', 'Sinop', NULL], + 'TR58' => ['Sivas', 'Sivas', NULL], + 'TR59' => ['Tekirdağ', 'Tekirdağ', NULL], + 'TR60' => ['Tokat', 'Tokat', NULL], + 'TR61' => ['Trabzon', 'Trabzon', NULL], + 'TR62' => ['Tunceli', 'Tunceli', NULL], + 'TR63' => ['Şanlıurfa', 'Şanlıurfa', NULL], + 'TR64' => ['Uşak', 'Uşak', NULL], + 'TR65' => ['Van', 'Van', NULL], + 'TR66' => ['Yozgat', 'Yozgat', NULL], + 'TR67' => ['Zonguldak', 'Zonguldak', NULL], + 'TR68' => ['Aksaray', 'Aksaray', NULL], + 'TR69' => ['Bayburt', 'Bayburt', NULL], + 'TR70' => ['Karaman', 'Karaman', NULL], + 'TR71' => ['Kırıkkale', 'Kırıkkale', NULL], + 'TR72' => ['Batman', 'Batman', NULL], + 'TR73' => ['Şırnak', 'Şırnak', NULL], + 'TR74' => ['Bartın', 'Bartın', NULL], + 'TR75' => ['Ardahan', 'Ardahan', NULL], + 'TR76' => ['Iğdır', 'Iğdır', NULL], + 'TR77' => ['Yalova', 'Yalova', NULL], + 'TR78' => ['Karabük', 'Karabük', NULL], + 'TR79' => ['Kilis', 'Kilis', NULL], + 'TR80' => ['Osmaniye', 'Osmaniye', NULL], + 'TR81' => ['Düzce', 'Düzce', NULL], + ], + // Tanzania. + 'TZ' => [], + // Uganda. + 'UG' => [], + // United States Minor Outlying Islands. + 'UM' => [], + // United States. + 'US' => [ + 'AL' => ['AL', 'Alabama', NULL], + 'AK' => ['AK', 'Alaska', NULL], + 'AZ' => ['AZ', 'Arizona', NULL], + 'AR' => ['AR', 'Arkansas', NULL], + 'CA' => ['CA', 'California', NULL], + 'CO' => ['CO', 'Colorado', NULL], + 'CT' => ['CT', 'Connecticut', NULL], + 'DE' => ['DE', 'Delaware', NULL], + 'DC' => ['DC', 'District of Columbia', NULL], + 'FL' => ['FL', 'Florida', NULL], + 'GA' => ['GA', 'Georgia', NULL], + 'HI' => ['HI', 'Hawaii', NULL], + 'ID' => ['ID', 'Idaho', NULL], + 'IL' => ['IL', 'Illinois', NULL], + 'IN' => ['IN', 'Indiana', NULL], + 'IA' => ['IA', 'Iowa', NULL], + 'KS' => ['KS', 'Kansas', NULL], + 'KY' => ['KY', 'Kentucky', NULL], + 'LA' => ['LA', 'Louisiana', NULL], + 'ME' => ['ME', 'Maine', NULL], + 'MD' => ['MD', 'Maryland', NULL], + 'MA' => ['MA', 'Massachusetts', NULL], + 'MI' => ['MI', 'Michigan', NULL], + 'MN' => ['MN', 'Minnesota', NULL], + 'MS' => ['MS', 'Mississippi', NULL], + 'MO' => ['MO', 'Missouri', NULL], + 'MT' => ['MT', 'Montana', NULL], + 'NE' => ['NE', 'Nebraska', NULL], + 'NV' => ['NV', 'Nevada', NULL], + 'NH' => ['NH', 'New Hampshire', NULL], + 'NJ' => ['NJ', 'New Jersey', NULL], + 'NM' => ['NM', 'New Mexico', NULL], + 'NY' => ['NY', 'New York', NULL], + 'NC' => ['NC', 'North Carolina', NULL], + 'ND' => ['ND', 'North Dakota', NULL], + 'OH' => ['OH', 'Ohio', NULL], + 'OK' => ['OK', 'Oklahoma', NULL], + 'OR' => ['OR', 'Oregon', NULL], + 'PA' => ['PA', 'Pennsylvania', NULL], + 'RI' => ['RI', 'Rhode Island', NULL], + 'SC' => ['SC', 'South Carolina', NULL], + 'SD' => ['SD', 'South Dakota', NULL], + 'TN' => ['TN', 'Tennessee', NULL], + 'TX' => ['TX', 'Texas', NULL], + 'UT' => ['UT', 'Utah', NULL], + 'VT' => ['VT', 'Vermont', NULL], + 'VA' => ['VA', 'Virginia', NULL], + 'WA' => ['WA', 'Washington', NULL], + 'WV' => ['WV', 'West Virginia', NULL], + 'WI' => ['WI', 'Wisconsin', NULL], + 'WY' => ['WY', 'Wyoming', NULL], + 'AA' => ['AA', 'Armed Forces (AA)', NULL], + 'AE' => ['AE', 'Armed Forces (AE)', NULL], + 'AP' => ['AP', 'Armed Forces (AP)', NULL], + //[ 'AS', 'American Samoa', NULL ], + //[ 'GU', 'Guam', NULL ], + //[ 'MH', 'Marshall Islands', NULL ], + //[ 'FM', 'Micronesia', NULL ], + //[ 'MP', 'Northern Mariana Islands', NULL ], + //[ 'PW', 'Palau', NULL ], + //[ 'PR', 'Puerto Rico', NULL ], + //[ 'VI', 'Virgin Islands', NULL ], + ], + // Vietnam. + 'VN' => [], + // Mayotte. + 'YT' => [], + // South Africa. + 'ZA' => [], + // Zambia. + 'ZM' => [], + ]; + // phpcs:enable +} + +/** + * WC_Stripe_Express_Checkout_Helper class. + */ +class WC_Stripe_Express_Checkout_Helper { + + /** + * Sanitize string for comparison. + * + * @param string $string String to be sanitized. + * + * @return string The sanitized string. + */ + public function sanitize_string( $string ) { + return trim( strtolower( $string ) ); + } + + /** + * Get normalized state from express checkout API dropdown list of states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_pr_states( $state, $country ) { + // Include Payment Request API State list for compatibility with WC countries/states. + $pr_states = WC_Stripe_Payment_Request_Button_States::STATES; + + if ( ! isset( $pr_states[ $country ] ) ) { + return $state; + } + + foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { + $sanitized_state_string = $this->sanitize_string( $state ); + // Checks if input state matches with Payment Request state code (0), name (1) or localName (2). + if ( + ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) || + ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) || + ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) ) + ) { + return $wc_state_abbr; + } + } + + return $state; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12767.php b/tests/PHPStan/Analyser/data/bug-12767.php new file mode 100644 index 0000000000..8ba79bff66 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12767.php @@ -0,0 +1,19 @@ + ['dd1' => 1, 'dd2' => 2]]; + + for ($i=1; $i <= 2; $i++) { + ${'field'.$i} = $employee->data['dd'.$i]; + + assertType('int', ${'field'.$i}); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12778.php b/tests/PHPStan/Analyser/data/bug-12778.php new file mode 100644 index 0000000000..23e4039715 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12778.php @@ -0,0 +1,13 @@ +{''}; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12787.php b/tests/PHPStan/Analyser/data/bug-12787.php new file mode 100644 index 0000000000..189d88cc8b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12787.php @@ -0,0 +1,22 @@ + $labels + */ + public function b(array $labels, \stdClass $payment): bool + { + $fullData = ' + { + "additionalData": { + "acquirerAccountCode": "TestPmmAcquirerAccount", + "authorisationMid": "1009", + "cvcResult": "1 Matches", + "avsResult": "4 AVS not supported for this card type", + "authCode": "25595", + "acquirerReference": "acquirerReference", + "expiryDate": "8/2018", + "avsResultRaw": "Y", + "cvcResultRaw": "M", + "refusalReasonRaw": "00 : Approved or completed successfully", + "refusalCodeRaw": "00", + "acquirerCode": "TestPmmAcquirer", + "inferredRefusalReason": "3D Secure Mandated", + "networkTxReference": "MCC123456789012", + "cardHolderName": "Test Cardholder", + "issuerCountry": "NL", + "countryCode": "NL", + "cardBin": "411111", + "issuerBin": "41111101", + "cardSchemeCommercial": "true", + "cardPaymentMethod": "visa", + "cardIssuingBank": "Bank of America", + "cardIssuingCountry": "US", + "cardIssuingCurrency": "USD", + "fundingSource": "PREPAID_RELOADABLE", + "cardSummary": "1111", + "isCardCommercial": "true", + "paymentMethodVariant": "visadebit", + "paymentMethod": "visa", + "coBrandedWith": "visa", + "businessTypeIdentifier": "PP", + "cardProductId": "P", + "bankSummary": "1111", + "bankAccount.ownerName": "A. Klaassen", + "bankAccount.iban": "NL13TEST0123456789", + "cavv": "AQIDBAUGBw", + "xid": "ODgxNDc2MDg2", + "cavvAlgorithm": "3", + "eci": "02", + "dsTransID": "f8062b92-66e9-4c5a-979a-f465e66a6e48", + "threeDSVersion": "2.1.0", + "threeDAuthenticatedResponse": "Y", + "liabilityShift": "true", + "threeDOffered": "true", + "threeDAuthenticated": "false", + "challengeCancel": "01", + "fraudResultType": "FRAUD", + "fraudManualReview": "false" + }, + "fraudResult": { + "accountScore": 10, + "result": { + "fraudCheckResult": { + "accountScore": "10", + "checkId": "26", + "name": "ShopperEmailRefCheck" + } + } + }, + "response": "[cancelOrRefund-received]" + }'; + + $result = json_decode($fullData, true); + + $r = $labels['result_code'] === '' + && $labels['merchant_reference'] === $payment->merchant_reference + && $labels['brand_code'] === $payment->brand_code + && $labels['acquirer_account_code'] === $result['additionalData']['acquirerAccountCode'] + && $labels['authorisation_mid'] === $result['additionalData']['authorisationMid'] + && $labels['cvc_result'] === $result['additionalData']['cvcResult'] + && $labels['auth_code'] === $result['additionalData']['authCode'] + && $labels['acquirer_reference'] === $result['additionalData']['acquirerReference'] + && $labels['expiry_date'] === $result['additionalData']['expiryDate'] + && $labels['avs_result_raw'] === $result['additionalData']['avsResultRaw'] + && $labels['cvc_result_raw'] === $result['additionalData']['cvcResultRaw'] + && $labels['acquirer_code'] === $result['additionalData']['acquirerCode'] + && $labels['inferred_refusal_reason'] === $result['additionalData']['inferredRefusalReason'] + && $labels['network_tx_reference'] === $result['additionalData']['networkTxReference'] + && $labels['issuer_country'] === $result['additionalData']['issuerCountry'] + && $labels['country_code'] === $result['additionalData']['countryCode'] + && $labels['card_bin'] === $result['additionalData']['cardBin'] + && $labels['issuer_bin'] === $result['additionalData']['issuerBin'] + && $labels['card_scheme_commercial'] === $result['additionalData']['cardSchemeCommercial'] + && $labels['card_payment_method'] === $result['additionalData']['cardPaymentMethod'] + && $labels['card_issuing_bank'] === $result['additionalData']['cardIssuingBank'] + && $labels['card_issuing_country'] === $result['additionalData']['cardIssuingCountry'] + && $labels['card_issuing_currency'] === $result['additionalData']['cardIssuingCurrency'] + && $labels['card_summary'] === $result['additionalData']['cardSummary'] + && $labels['payment_method_variant'] === $result['additionalData']['paymentMethodVariant'] + && $labels['payment_method'] === $result['additionalData']['paymentMethod'] + && $labels['co_branded_with'] === $result['additionalData']['coBrandedWith'] + && $labels['business_type_identifier'] === $result['additionalData']['businessTypeIdentifier'] + && $labels['card_product_id'] === $result['additionalData']['cardProductId'] + && $labels['bank_summary'] === $result['additionalData']['bankSummary'] + && $labels['cavv'] === $result['additionalData']['cavv'] + && $labels['xid'] === $result['additionalData']['xid'] + && $labels['cavv_algorithm'] === $result['additionalData']['cavvAlgorithm'] + && $labels['eci'] === $result['additionalData']['eci'] + && $labels['ds_trans_id'] === $result['additionalData']['dsTransID'] + && $labels['liability_shift'] === $result['additionalData']['liabilityShift'] + && $labels['fraud_result_type'] === $result['additionalData']['fraudResultType'] + && $labels['fraud_manual_review'] === $result['additionalData']['fraudManualReview'] + && $labels['fraud_result_account_score'] === $result['fraudResult']['accountScore'] + && $labels['fraud_result_check_id'] === $result['fraudResult']['result']['fraudCheckResult']['checkId'] + && $labels['fraud_result_name'] === $result['fraudResult']['result']['fraudCheckResult']['name'] + && $labels['response'] === $result['response']; + return $r; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12803.php b/tests/PHPStan/Analyser/data/bug-12803.php new file mode 100644 index 0000000000..60e6ecf05c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12803.php @@ -0,0 +1,27 @@ + $a */ + $a = $this->c(fn() => (object) ['bar' => 1, 'foo' => 2]); + $b = $this->c(fn() => (object) ['bar' => 1, 'foo' => 2]); + } + + /** + * @template T + * @param callable(): T $callback + * @return Generic + */ + public function c(callable $callback): Generic + { + return new Generic(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12934.php b/tests/PHPStan/Analyser/data/bug-12934.php new file mode 100644 index 0000000000..36109899a8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12934.php @@ -0,0 +1,9 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12934; + +function(string $path): void { + session_set_cookie_params(0, path: $path, secure: true, httponly: true); +}; diff --git a/tests/PHPStan/Analyser/data/bug-12949.php b/tests/PHPStan/Analyser/data/bug-12949.php new file mode 100644 index 0000000000..eeafccb0de --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12949.php @@ -0,0 +1,19 @@ +{$b}(); + $o::{$b}(); + echo $o::{$b}; + + echo ""; +} diff --git a/tests/PHPStan/Analyser/data/bug-12979.php b/tests/PHPStan/Analyser/data/bug-12979.php new file mode 100644 index 0000000000..5cd103b227 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12979.php @@ -0,0 +1,21 @@ +acceptNonEmptyString($this->callableString()); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-13057.php b/tests/PHPStan/Analyser/data/bug-13057.php new file mode 100644 index 0000000000..9b9c4eabae --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13057.php @@ -0,0 +1,25 @@ +modelB->extra); + assertType('string', $b->modelA->extra); +}; diff --git a/tests/PHPStan/Analyser/data/bug-13129-php7.php b/tests/PHPStan/Analyser/data/bug-13129-php7.php new file mode 100644 index 0000000000..c12e1db1a6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13129-php7.php @@ -0,0 +1,9 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13218; + +/** + * @template TSteps of iterable|int + */ +class Progress +{ + public mixed $total = 0; + + /** + * Create a new ProgressBar instance. + * + * @param TSteps $steps + */ + public function __construct(public string $label, public iterable|int $steps, public string $hint = '') + { + $this->total = match (true) { + is_int($this->steps) => $this->steps, + is_countable($this->steps) => count($this->steps), + is_iterable($this->steps) => iterator_count($this->steps), + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-13279.php b/tests/PHPStan/Analyser/data/bug-13279.php new file mode 100644 index 0000000000..feb687a3ed --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13279.php @@ -0,0 +1,18 @@ + + */ +function get_array(): array +{ + return []; +} + +$tests = get_array(); + +foreach ($tests as $test) { + +// information fichiers + $test['information'] = ''; + $test['information'] .= $test['a'] ? 'test' : ''; + $test['information'] .= $test['b'] ? 'test' : ''; + $test['information'] .= $test['c'] ? 'test' : ''; + $test['information'] .= $test['d'] ? 'test' : ''; + $test['information'] .= $test['e'] ? 'test' : ''; + $test['information'] .= $test['f'] ? 'test' : ''; + $test['information'] .= $test['g'] ? 'test' : ''; + $test['information'] .= $test['h'] ? 'test' : ''; + +} diff --git a/tests/PHPStan/Analyser/data/bug-13352.php b/tests/PHPStan/Analyser/data/bug-13352.php new file mode 100644 index 0000000000..49f116938a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13352.php @@ -0,0 +1,4055 @@ +coalesce( + [ + $var1['key520'], + $var1['key521'], + $var1['key522'], + ], + ); + + /** + * Parse document multi benchmark + * Parse evethings related to multi benchmark data + * */ + + $var11 = 11; + if ( + $var1['key72'] == true + && $var1['key61'] == 1 + ) { + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var1['key515' . $var12] = $var10->coalesce( + [ + $var1['key514' . $var12], + $var1['key516' . $var12], + $var1['key517' . $var12], + ], + ); + $var1['key523' . $var12] = $var1['key524' . $var12]; + } + +//Data for template without breaking the checkSum + $var2['key525'] = true; + } else { +//Data for template without breaking the checkSum + $var2['key525'] = false; + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var1['key515' . $var12] = null; + $var1['key523' . $var12] = null; + } + } + + if (!is_null($var1['key526'])) { + $var1['key527'] + = $var1['key526']; + } elseif (!is_null($var1['key528'])) { + $var1['key527'] + = $var1['key528']; + } else { + $var1['key527'] + = $var1['key529']; + } + $var1['key530'] = $var10->coalesce( + [ + $var1['key531'], + $var1['key532'], + ], + ); + $var1['key533'] = $var10->coalesce( + [ + $var1['key534'], + $var1['key535'], + ], + ); + if (!is_null($var1['key536'])) { + $var1['key537'] + = $var1['key536']; + } elseif (!is_null($var1['key538'])) { + $var1['key537'] + = $var1['key538']; + } else { + $var1['key537'] + = $var1['key539']; + } + if (!is_null($var1['key540'])) { + $var1['key541'] = $var1['key540']; + } elseif (!is_null($var1['key542'])) { + $var1['key541'] = $var1['key542']; + } else { + $var1['key541'] = $var1['key543']; + } + + $var1['key544'] + = $var1['key545']; + if (!is_null($var1['key546'])) { + $var1['key547'] + = $var1['key546']; + } elseif (!is_null($var1['key548'])) { + $var1['key547'] + = $var1['key548']; + } else { + $var1['key547'] + = $var1['key549']; + } + if (!is_null($var1['key550'])) { + $var1['key551'] + = $var1['key550']; + } elseif (!is_null($var1['key552'])) { + $var1['key551'] + = $var1['key552']; + } else { + $var1['key551'] + = $var1['key553']; + } + if (!is_null($var1['key554'])) { + $var1['key555'] + = $var1['key554']; + } elseif (!is_null($var1['key556'])) { + $var1['key555'] + = $var1['key556']; + } else { + $var1['key555'] + = $var1['key557']; + } + if (!is_null($var1['key558'])) { + $var1['key559'] + = $var1['key558']; + } elseif (!is_null($var1['key560'])) { + $var1['key559'] + = $var1['key560']; + } else { + $var1['key559'] + = $var1['key561']; + } + if (!is_null($var1['key562'])) { + $var1['key563'] + = $var1['key562']; + } elseif (!is_null($var1['key564'])) { + $var1['key563'] + = $var1['key564']; + } else { + $var1['key563'] + = $var1['key565']; + } + if (!is_null($var1['key566'])) { + $var1['key567'] + = $var1['key566']; + } elseif (!is_null($var1['key568'])) { + $var1['key567'] + = $var1['key568']; + } else { + $var1['key567'] + = $var1['key569']; + } + if (!is_null($var1['key570'])) { + $var1['key571'] + = $var1['key570']; + } elseif (!is_null($var1['key572'])) { + $var1['key571'] + = $var1['key572']; + } else { + $var1['key571'] + = $var1['key573']; + } + if (!is_null($var1['key574'])) { + $var1['key575'] + = $var1['key574']; + } elseif (!is_null($var1['key576'])) { + $var1['key575'] + = $var1['key576']; + } else { + $var1['key575'] + = $var1['key577']; + } + $var1['key578'] = $var10->coalesce( + [ + $var1['key579'], + $var1['key580'], + ], + ); + $var1['key581'] = $var10->coalesce( + [ + $var1['key582'], + $var1['key583'], + ], + ); + $var1['key584'] = $var10->coalesce( + [ + $var1['key585'], + $var1['key586'], + ], + ); + $var1['key587'] = $var10->coalesce( + [ + $var1['key588'], + $var1['key589'], + ], + ); + $var1['key590'] = $var10->coalesce( + [ + $var1['key591'], + $var1['key592'], + ], + ); + $var1['key593'] = $var10->coalesce( + [ + $var1['key594'], + $var1['key595'], + ], + ); + $var1['key596'] = $var10->coalesce( + [ + $var1['key597'], + $var1['key598'], + ], + ); + $var1['key599'] = $var10->coalesce( + [ + $var1['key600'], + $var1['key601'], + ], + ); + if (!is_null($var1['key602'])) { + $var1['key603'] = $var1['key602']; + } elseif (!is_null($var1['key604'])) { + $var1['key603'] = $var1['key604']; + } else { + $var1['key603'] = $var1['key605']; + } + if (!is_null($var1['key606'])) { + $var1['key607'] = $var1['key606']; + } elseif (!is_null($var1['key608'])) { + $var1['key607'] = $var1['key608']; + } else { + $var1['key607'] = $var1['key609']; + } + if (!is_null($var1['key610'])) { + $var1['key611'] = $var1['key610']; + } elseif (!is_null($var1['key612'])) { + $var1['key611'] = $var1['key612']; + } else { + $var1['key611'] = $var1['key613']; + } + if (!is_null($var1['key614'])) { + $var1['key615'] = $var1['key614']; + } elseif (!is_null($var1['key616'])) { + $var1['key615'] = $var1['key616']; + } else { + $var1['key615'] = $var1['key617']; + } + if (!is_null($var1['key618'])) { + $var1['key619'] = $var1['key618']; + } elseif (!is_null($var1['key620'])) { + $var1['key619'] = $var1['key620']; + } else { + $var1['key619'] = $var1['key621']; + } + if (!is_null($var1['key622'])) { + $var1['key623'] = $var1['key622']; + } elseif (!is_null($var1['key624'])) { + $var1['key623'] = $var1['key624']; + } else { + $var1['key623'] = $var1['key625']; + } + if (!is_null($var1['key626'])) { + $var1['key627'] + = $var1['key626']; + } elseif (!is_null($var1['key628'])) { + $var1['key627'] + = $var1['key628']; + } else { + $var1['key627'] + = $var1['key629']; + } + if (!is_null($var1['key630'])) { + $var1['key631'] + = $var1['key630']; + } elseif (!is_null($var1['key632'])) { + $var1['key631'] + = $var1['key632']; + } else { + $var1['key631'] + = $var1['key633']; + } + if (!is_null($var1['key634'])) { + $var1['key635'] + = $var1['key634']; + } elseif (!is_null($var1['key636'])) { + $var1['key635'] + = $var1['key636']; + } else { + $var1['key635'] + = $var1['key637']; + } + if (!is_null($var1['key638'])) { + $var1['key639'] + = $var1['key638']; + } elseif (!is_null($var1['key640'])) { + $var1['key639'] + = $var1['key640']; + } else { + $var1['key639'] + = $var1['key641']; + } + if (!is_null($var1['key642'])) { + $var1['key643'] + = $var1['key642']; + } elseif (!is_null($var1['key644'])) { + $var1['key643'] + = $var1['key644']; + } else { + $var1['key643'] + = $var1['key645']; + } + if (!is_null($var1['key646'])) { + $var1['key647'] + = $var1['key646']; + } elseif (!is_null($var1['key648'])) { + $var1['key647'] + = $var1['key648']; + } else { + $var1['key647'] + = $var1['key649']; + } + + $var1['key650'] + = $var1['key651']; + if (isset($var1['key652'])) { + $var1['key653'] + = $var1['key652']; + } elseif (isset($var1['key654'])) { + $var1['key653'] + = $var1['key654']; + } elseif (isset($var1['key655'])) { + $var1['key653'] + = $var1['key655']; + } elseif (isset($var1['key656'])) { + $var1['key653'] + = $var1['key656']; + } elseif (isset($var1['key657'])) { + $var1['key653'] + = $var1['key657']; + } elseif (isset($var1['key658'])) { + $var1['key653'] + = $var1['key658']; + } else { + $var1['key653'] = null; + } + $var1['key659'] = !is_null( + $var1['key660'], + ) ? $var1['key660'] + : $var1['key661']; + $var1['key662'] + = $var1['key663']; + + if (isset($var1['key9']) && $var1['key9'] == 'key441') { + if (isset($var1['key664'])) { + $var1['key665'] + = $var1['key664']; + } else { + $var1['key665'] = null; + } + } + if (empty($var1['key665'])) { + if (isset($var1['key666'])) { + $var1['key665'] + = $var1['key666']; + } else { + $var1['key665'] = null; + } + } + + if (!is_null($var1['key667'])) { + $var1['key668'] + = $var1['key667']; + } elseif (!is_null($var1['key669'])) { + $var1['key668'] + = $var1['key669']; + } else { + $var1['key668'] + = $var1['key670']; + } + $var1['key671'] + = $var1['key672']; + + if (isset($var1['key9']) && $var1['key9'] == 'key441') { + if (!is_null($var1['key673'])) { + $var1['key674'] + = $var1['key673']; + } elseif (!is_null($var1['key675'])) { + $var1['key674'] + = $var1['key675']; + } else { + $var1['key674'] + = $var1['key676']; + } + } + if (empty($var1['key674'])) { + if (!is_null($var1['key677'])) { + $var1['key674'] = $var1['key677']; + } elseif (!is_null($var1['key678'])) { + $var1['key674'] = $var1['key678']; + } else { + $var1['key674'] = $var1['key679']; + } + } + + if (!is_null($var1['key680'])) { + $var1['key681'] = $var1['key680']; + } elseif (!is_null($var1['key682'])) { + $var1['key681'] = $var1['key682']; + } else { + $var1['key681'] = $var1['key683']; + } + $var1['key684'] + = $var1['key685']; + if (isset($var1['key686'])) { + $var1['key687'] + = $var1['key686']; + } elseif (isset($var1['key688'])) { + $var1['key687'] + = $var1['key688']; + } + $var1['key689'] = !is_null($var1['key690']) + ? $var1['key690'] : $var1['key691']; + $var1['key692'] = !is_null($var1['key693']) + ? $var1['key693'] : $var1['key694']; + $var1['key695'] = !is_null($var1['key696']) + ? $var1['key696'] : $var1['key697']; + $var1['key698'] = !is_null($var1['key699']) + ? $var1['key699'] : $var1['key700']; + $var1['key701'] = !is_null($var1['key702']) + ? $var1['key702'] : $var1['key703']; + $var1['key704'] = !is_null($var1['key705']) + ? $var1['key705'] : $var1['key706']; + $var1['key707'] = !is_null($var1['key708']) + ? $var1['key708'] : $var1['key709']; + $var1['key710'] = !is_null($var1['key711']) + ? $var1['key711'] : $var1['key712']; + if (!is_null($var1['key713'])) { + $var1['key714'] + = $var1['key713']; + } elseif (!is_null($var1['key715'])) { + $var1['key714'] + = $var1['key715']; + } else { + $var1['key714'] + = $var1['key716']; + } + if (!is_null($var1['key717'])) { + $var1['key718'] + = $var1['key717']; + } elseif (!is_null($var1['key719'])) { + $var1['key718'] + = $var1['key719']; + } else { + $var1['key718'] + = $var1['key720']; + } + if (!is_null($var1['key721'])) { + $var1['key722'] + = $var1['key721']; + } elseif (!is_null($var1['key723'])) { + $var1['key722'] + = $var1['key723']; + } else { + $var1['key722'] + = $var1['key724']; + } + if (!is_null($var1['key725'])) { + $var1['key726'] = $var1['key725']; + } elseif (!is_null($var1['key727'])) { + $var1['key726'] = $var1['key727']; + } else { + $var1['key726'] = $var1['key728']; + } + if (!is_null($var1['key729'])) { + $var1['key730'] = $var1['key729']; + } elseif (!is_null($var1['key731'])) { + $var1['key730'] = $var1['key731']; + } else { + $var1['key730'] = $var1['key732']; + } + if (!is_null($var1['key733'])) { + $var1['key734'] = $var1['key733']; + } elseif (!is_null($var1['key735'])) { + $var1['key734'] = $var1['key735']; + } else { + $var1['key734'] = $var1['key736']; + } + if (!is_null($var1['key737'])) { + $var1['key738'] = $var1['key737']; + } elseif (!is_null($var1['key739'])) { + $var1['key738'] = $var1['key739']; + } else { + $var1['key738'] = $var1['key740']; + } + if (!is_null($var1['key741'])) { + $var1['key742'] = $var1['key741']; + } elseif (!is_null($var1['key743'])) { + $var1['key742'] = $var1['key743']; + } else { + $var1['key742'] = $var1['key744']; + } + +//OTHERS FIELDS + $var1['key745'] = $var1['key17']; + $var1['key746'] = $var1['key18']; + $var1['key747'] = $var1['key19']; + $var1['key748'] = $var1['key20']; + $var1['key749'] = !is_null($var1['key22']) + ? $var1['key22'] : $var1['key23']; + + $var1['key750'] = !is_null($var1['key35']) + ? $var1['key35'] : $var1['key36']; + $var1['key751'] = $var1['key37']; + +//HACK comment:74:ticket:1246 + if ($var1['key67'] == 305) { + if (empty($var1['key11'])) { + $var1['key474'] = null; + $var1['key478'] = null; + $var1['key448'] = null; + } else { + $var1['key478'] = $var1['key11'] . '%'; + } + $var1['key752'] = $var1['key38']; + $var1['key753'] = $var1['key39']; + $var1['key754'] = $var1['key40']; + } else { + $var1['key752'] = !is_null($var1['key38']) ? $var1['key38'] + + $var1['key13'] : null; + $var1['key753'] = !is_null($var1['key39']) ? $var1['key39'] + + $var1['key11'] : null; + $var1['key754'] = !is_null($var1['key40']) ? $var1['key40'] + + $var1['key12'] : null; + } + + if (!is_null($var1['key41'])) { + $var13 = date( + 'key755', + strtotime( + $var1['key41'], + ), + ); + + $var14 = explode('-', $var1['key41']); + $var1['key756'] = $var14[0]; + $var1['key757'] = $var14[1]; + $var1['key758'] = + $var2["MonthName{$var13}"] ?? null; + $var1['key759'] = $var14[2]; + } else { + $var1['key756'] + = $var1['key757'] + = $var1['key758'] = $var1['key759'] = null; + } + if (!is_null($var1['key42'])) { + $var15 = date( + 'key755', + strtotime( + $var1['key42'], + ), + ); + + $var14 = explode('-', $var1['key42']); + $var1['key760'] = $var14[0]; + $var1['key761'] = $var14[1]; + $var1['key762'] = + $var2["MonthName{$var15}"] ?? null; + $var1['key763'] = $var14[2]; + } else { + $var1['key760'] + = $var1['key761'] + = $var1['key762'] = $var1['key763'] = null; + } + + $var16 = + $var1['key764'] === 'key513' ? $var1['key765'] + : $var1['key42']; + if (isset($var16)) { + $var17 = explode('-', $var16); + $var18 = $var17[0]; + $var19 = $var17[1]; + $var20 = date('key755', strtotime($var16)); + $var21 = !empty($var2["MonthName{$var20}"]) + ? $var2["MonthName{$var20}"] : ''; + $var22 = $var17[2]; + } + + $var1['key766'] = $var1['key43']; + $var1['key767'] = $var1['key44']; + $var1['key4'] = $var1['key5']; + if (!is_null($var1['key768'])) { + $var1['key769'] + = $var1['key768']; + } elseif (!is_null($var1['key770'])) { + $var1['key769'] + = $var1['key770']; + } else { + $var1['key769'] + = $var1['key771']; + } + $var24 = $var1; +//Fetch the right DocumentUniqueID regarding the template expectation + $var23 = $var24->db()->prepare( + '', + ); + $var23->execute([$var1['key56']]); + $var25 = $var23->fetch(PDO::FETCH_COLUMN); + $var26 = $var24->getTemplateDesign($var25); + if ($var2['key525'] && $var26['key772'] == 'key773') { + $var1['key774'] = $var1['key47']; + $var27 = $var1['key775']; + } elseif ($var2['key525']) { + $var1['key774'] = $var1['key49']; + $var27 = $var1['key776']; + } elseif ($var26['key772'] == 'key773') { + $var1['key774'] = $var1['key46']; + $var27 = $var1['key68']; + } else { + $var1['key774'] = $var1['key48']; + $var27 = $var1['key777']; + } + + $var1['key778'] = $var1['key779']; + $var1['key780'] = $var1['key781']; + $var1['key782'] = $var1['key50']; + $var1['key523'] = $var10->coalesce( + [ + $var1['key51'], + $var1['key52'], + ], + ); + $var1['key783'] = $var1['key34']; + $var1['key53'] = $var1['key53']; + $var1['key784'] = $var1['key54']; + $var1['key785'] = $var1['key55']; + $var1['key786'] = $var1['key56']; + $var1['key787'] = $var1['key57']; + $var1['key788'] = $var1['key59']; + $var1['key789'] = $var1['key60']; + $var1['key790'] = $var1['key70']; + $var1['key791'] = $var1['key64']; + $var1['key792'] = $var1['key65']; + $var1['key793'] = $var1['key66']; + $var1['key794'] = $var1['key71']; + $var1['key795'] + = $var1['key796']; + $var1['key797'] + = $var1['key798']; + $var1['key799'] = $var1['key800']; + $var1['key801'] + = $var1['key802']; + $var1['key803'] + = $var1['key804']; + $var1['key805'] + = $var1['key806']; + $var1['key807'] + = $var1['key808']; + $var1['key809'] + = $var1['key810']; + $var1['key811'] + = $var1['key812']; + $var1['key813'] + = $var1['key814']; + $var1['key815'] + = $var1['key816']; + $var1['key817'] + = $var1['key818']; + $var1['key819'] + = $var1['key820']; + $var1['key821'] + = $var1['key822']; +//UNSET useless fields + unset( + $var1['key73'], $var1['key75'], $var1['key76'], $var1['key77'], $var1['key79'], $var1['key80'], $var1['key81'], $var1['key83'], $var1['key84'], $var1['key85'], $var1['key87'], $var1['key88'], $var1['key89'], $var1['key91'], $var1['key92'], $var1['key93'], $var1['key95'], $var1['key96'], $var1['key97'], $var1['key99'], $var1['key100'], $var1['key101'], $var1['key103'], $var1['key104'], $var1['key106'], $var1['key108'], $var1['key110'], $var1['key112'], $var1['key114'], $var1['key115'], $var1['key117'], $var1['key118'], $var1['key119'], $var1['key121'], $var1['key122'], $var1['key124'], $var1['key126'], $var1['key127'], $var1['key129'], $var1['key131'], $var1['key132'], $var1['key134'], $var1['key136'], $var1['key138'], $var1['key139'], $var1['key141'], $var1['key142'], $var1['key144'], $var1['key145'], $var1['key146'], $var1['key148'], $var1['key149'], $var1['key150'], $var1['key152'], $var1['key153'], $var1['key154'], $var1['key156'], $var1['key157'], $var1['key158'], $var1['key160'], $var1['key161'], $var1['key162'], $var1['key164'], $var1['key165'], $var1['key166'], $var1['key168'], $var1['key169'], $var1['key170'], $var1['key172'], $var1['key173'], $var1['key174'], $var1['key176'], $var1['key177'], $var1['key178'], $var1['key180'], $var1['key181'], $var1['key182'], $var1['key184'], $var1['key185'], $var1['key187'], $var1['key189'], $var1['key191'], $var1['key193'], $var1['key195'], $var1['key197'], $var1['key199'], $var1['key201'], $var1['key202'], $var1['key204'], $var1['key205'], $var1['key207'], $var1['key208'], $var1['key210'], $var1['key211'], $var1['key213'], $var1['key214'], $var1['key216'], $var1['key217'], $var1['key219'], $var1['key220'], $var1['key222'], $var1['key223'], $var1['key225'], $var1['key226'], $var1['key228'], $var1['key229'], $var1['key231'], $var1['key232'], $var1['key234'], $var1['key235'], $var1['key236'], $var1['key238'], $var1['key239'], $var1['key240'], $var1['key242'], $var1['key243'], $var1['key244'], $var1['key246'], $var1['key247'], $var1['key248'], $var1['key250'], $var1['key251'], $var1['key252'], $var1['key254'], $var1['key255'], $var1['key256'], $var1['key258'], $var1['key259'], $var1['key260'], $var1['key262'], $var1['key263'], $var1['key264'], $var1['key266'], $var1['key267'], $var1['key269'], $var1['key271'], $var1['key273'], $var1['key275'], $var1['key276'], $var1['key278'], $var1['key280'], $var1['key282'], $var1['key284'], $var1['key286'], $var1['key288'], $var1['key290'], $var1['key292'], $var1['key294'], $var1['key295'], $var1['key296'], $var1['key298'], $var1['key299'], $var1['key301'], $var1['key303'], $var1['key305'], $var1['key307'], $var1['key309'], $var1['key311'], $var1['key312'], $var1['key314'], $var1['key315'], $var1['key317'], $var1['key318'], $var1['key320'], $var1['key321'], $var1['key323'], $var1['key324'], $var1['key326'], $var1['key327'], $var1['key329'], $var1['key330'], $var1['key332'], $var1['key333'], $var1['key335'], $var1['key336'], $var1['key338'], $var1['key339'], $var1['key341'], $var1['key342'], $var1['key344'], $var1['key345'], $var1['key347'], $var1['key348'], $var1['key350'], $var1['key351'], $var1['key353'], $var1['key354'], $var1['key356'], $var1['key357'], $var1['key359'], $var1['key360'], $var1['key362'], $var1['key363'], $var1['key365'], $var1['key367'], $var1['key369'], $var1['key371'], $var1['key372'], $var1['key374'], $var1['key375'], $var1['key376'], $var1['key378'], $var1['key379'], $var1['key380'], $var1['key382'], $var1['key383'], $var1['key384'], $var1['key386'], $var1['key387'], $var1['key388'], $var1['key390'], $var1['key391'], $var1['key392'], $var1['key394'], $var1['key395'], $var1['key396'], $var1['key398'], $var1['key399'], $var1['key400'], $var1['key402'], $var1['key21'], $var1['key403'], $var1['key405'], $var1['key406'], $var1['key408'], $var1['key410'], $var1['key414'], $var1['key413'], $var1['key411'], $var1['key418'], $var1['key417'], $var1['key415'], $var1['key422'], $var1['key421'], $var1['key419'], $var1['key426'], $var1['key425'], $var1['key423'], $var1['key427'], $var1['key429'], $var1['key430'], $var1['key432'], $var1['key434'], $var1['key435'], $var1['key437'], $var1['key439'], $var1['key440'], $var1['key445'], $var1['key446'], $var1['key447'], $var1['key449'], $var1['key450'], $var1['key452'], $var1['key454'], $var1['key456'], $var1['key460'], $var1['key459'], $var1['key457'], $var1['key464'], $var1['key463'], $var1['key461'], $var1['key468'], $var1['key467'], $var1['key465'], $var1['key469'], $var1['key471'], $var1['key472'], $var1['key473'], $var1['key475'], $var1['key476'], $var1['key477'], $var1['key479'], $var1['key480'], $var1['key481'], $var1['key483'], $var1['key484'], $var1['key485'], $var1['key487'], $var1['key488'], $var1['key489'], $var1['key491'], $var1['key492'], $var1['key442'], $var1['key444'], $var1['key493'], $var1['key495'], $var1['key496'], $var1['key630'], $var1['key632'], $var1['key633'], $var1['key634'], $var1['key636'], $var1['key637'], $var1['key638'], $var1['key640'], $var1['key641'], $var1['key642'], $var1['key644'], $var1['key645'], $var1['key646'], $var1['key648'], $var1['key649'], $var1['key500'], $var1['key499'], $var1['key497'], $var1['key501'], $var1['key503'], $var1['key504'], $var1['key505'], $var1['key507'], $var1['key508'], $var1['key509'], $var1['key511'], $var1['key512'], $var1['key514'], $var1['key516'], $var1['key517'], $var1['key823'], $var1['key824'], $var1['key825'], $var1['key826'], $var1['key827'], $var1['key828'], $var1['key829'], $var1['key830'], $var1['key831'], $var1['key832'], $var1['key833'], $var1['key834'], $var1['key835'], $var1['key836'], $var1['key837'], $var1['key838'], $var1['key839'], $var1['key840'], $var1['key841'], $var1['key842'], $var1['key843'], $var1['key844'], $var1['key845'], $var1['key846'], $var1['key847'], $var1['key848'], $var1['key849'], $var1['key850'], $var1['key851'], $var1['key852'], $var1['key526'], $var1['key528'], $var1['key529'], $var1['key532'], $var1['key531'], $var1['key535'], $var1['key534'], $var1['key536'], $var1['key538'], $var1['key539'], $var1['key540'], $var1['key542'], $var1['key543'], $var1['key545'], $var1['key546'], $var1['key548'], $var1['key549'], $var1['key550'], $var1['key552'], $var1['key553'], $var1['key554'], $var1['key556'], $var1['key557'], $var1['key558'], $var1['key560'], $var1['key561'], $var1['key562'], $var1['key564'], $var1['key565'], $var1['key566'], $var1['key568'], $var1['key569'], $var1['key570'], $var1['key572'], $var1['key573'], $var1['key574'], $var1['key576'], $var1['key577'], $var1['key580'], $var1['key583'], $var1['key586'], $var1['key589'], $var1['key579'], $var1['key582'], $var1['key585'], $var1['key588'], $var1['key592'], $var1['key591'], $var1['key595'], $var1['key594'], $var1['key598'], $var1['key597'], $var1['key601'], $var1['key600'], $var1['key602'], $var1['key604'], $var1['key605'], $var1['key606'], $var1['key608'], $var1['key609'], $var1['key610'], $var1['key612'], $var1['key613'], $var1['key614'], $var1['key616'], $var1['key617'], $var1['key618'], $var1['key620'], $var1['key621'], $var1['key622'], $var1['key624'], $var1['key625'], $var1['key626'], $var1['key628'], $var1['key629'], $var1['key651'], $var1['key652'], $var1['key654'], $var1['key655'], $var1['key660'], $var1['key661'], $var1['key663'], $var1['key666'], $var1['key664'], $var1['key667'], $var1['key669'], $var1['key670'], $var1['key672'], $var1['key677'], $var1['key678'], $var1['key679'], $var1['key673'], $var1['key675'], $var1['key676'], $var1['key680'], $var1['key682'], $var1['key683'], $var1['key685'], $var1['key686'], $var1['key688'], $var1['key656'], $var1['key657'], $var1['key658'], $var1['key690'], $var1['key691'], $var1['key693'], $var1['key694'], $var1['key696'], $var1['key697'], $var1['key699'], $var1['key700'], $var1['key702'], $var1['key703'], $var1['key705'], $var1['key706'], $var1['key708'], $var1['key709'], $var1['key711'], $var1['key712'], $var1['key713'], $var1['key715'], $var1['key716'], $var1['key717'], $var1['key719'], $var1['key720'], $var1['key721'], $var1['key723'], $var1['key724'], $var1['key725'], $var1['key727'], $var1['key728'], $var1['key729'], $var1['key731'], $var1['key732'], $var1['key733'], $var1['key735'], $var1['key736'], $var1['key737'], $var1['key739'], $var1['key740'], $var1['key741'], $var1['key743'], $var1['key744'], $var1['key62'], $var1['key17'], $var1['key18'], $var1['key19'], $var1['key20'], $var1['key22'], $var1['key23'], $var1['key24'], $var1['key25'], $var1['key26'], $var1['key27'], $var1['key28'], $var1['key29'], $var1['key30'], $var1['key31'], $var1['key32'], $var1['key33'], $var1['key34'], $var1['key36'], $var1['key35'], $var1['key37'], $var1['key38'], $var1['key39'], $var1['key40'], $var1['key11'], $var1['key12'], $var1['key13'], $var1['key41'], $var1['key43'], $var1['key44'], $var1['key5'], $var1['key768'], $var1['key770'], $var1['key771'], $var1['key779'], $var1['key781'], $var1['key46'], $var1['key47'], $var1['key50'], $var1['key52'], $var1['key49'], $var1['key48'], $var1['key54'], $var1['key55'], $var1['key56'], $var1['key57'], $var1['key59'], $var1['key60'], $var1['key70'], $var1['key64'], $var1['key65'], $var1['key66'], $var1['key71'], $var1['key796'], $var1['key798'], $var1['key800'], $var1['key802'], $var1['key804'], $var1['key806'], $var1['key808'], $var1['key810'], $var1['key812'], $var1['key814'], $var1['key816'], $var1['key818'], $var1['key820'], $var1['key822'], $var1['key62'], $var1['key520'], $var1['key521'], $var1['key522'], $var1['key51'], + ); +//Control Data for bot + if ($var3 == 'key2') { + $var5['key63'] = $var1['key63']; + $var5['key753'] = $var1['key753']; + $var5['key754'] = $var1['key754']; + $var5['key4'] = $var1['key4']; + $var5['key748'] = $var1['key748']; + $var5['key431'] = $var1['key431']; + $var5['key853'] = $var1['key774']; + } +//End control + switch ($var1['key791']) { + case 'key854': + $var1['key855'] = $var1['key200']; + $var28 = !is_null($var1['key799']) + ? $var1['key799'] : ''; + break; + case 'key856': + $var1['key855'] + = $var1['key203']; + $var28 = !is_null($var1['key801']) + ? $var1['key801'] + : $var1['key791']; + break; + case 'key857': + $var1['key855'] + = $var1['key206']; + $var28 = !is_null($var1['key803']) + ? $var1['key803'] : $var1['key791']; + break; + case 'key858': + $var1['key855'] = $var1['key209']; + $var28 = !is_null($var1['key805']) + ? $var1['key805'] : $var1['key791']; + break; + case 'key859': + $var1['key855'] + = $var1['key212']; + $var28 = !is_null($var1['key807']) + ? $var1['key807'] : $var1['key791']; + break; + case 'key860': + $var1['key855'] = $var1['key215']; + $var28 = !is_null($var1['key809']) + ? $var1['key809'] : $var1['key791']; + break; + case 'key861': + $var1['key855'] + = $var1['key218']; + $var28 = !is_null($var1['key811']) + ? $var1['key811'] + : $var1['key791']; + break; + case 'key862': + $var1['key855'] = $var1['key221']; + $var28 = !is_null($var1['key813']) + ? $var1['key813'] : $var1['key791']; + break; + case 'key863': + $var1['key855'] = $var1['key224']; + $var28 = !is_null($var1['key815']) + ? $var1['key815'] : $var1['key791']; + break; + case 'key864': + $var1['key855'] = $var1['key227']; + $var28 = !is_null($var1['key817']) + ? $var1['key817'] : $var1['key791']; + break; + case 'key865': + $var1['key855'] = $var1['key230']; + $var28 = !is_null($var1['key819']) + ? $var1['key819'] : $var1['key791']; + break; + case 'multi-currency_hedge': + $var1['key855'] = + $var1['key233']; + $var28 = !is_null($var1['key821']) + ? $var1['key821'] : $var1['key791']; + break; + case null: + $var1['key855'] = ''; + $var28 = $var1['key791']; + break; + default: + $var28 = $var1['key791']; + break; + } + switch ($var1['key745']) { + case 'key866': + case 'Cap&Dist': + $var1['key867'] = $var1['key116']; + $var29 = !is_null($var1['key797']) + ? $var1['key797'] : $var1['key745']; + break; + case 'key868'; + $var1['key867'] = $var1['key120']; + $var29 = !is_null($var1['key795']) + ? $var1['key795'] : $var1['key745']; + break; + case null: + $var1['key867'] = ''; + $var29 = $var1['key745']; + break; + default: + $var29 = $var1['key745']; + break; + } + if (!empty($var1['key4'])) { + switch ($var1['key4']) { + case 1: + if (!is_null($var1['key279'])) { + $var1['key277'] + = $var1['key279']; + } + $var1['key143'] = !is_null( + $var1['key147'], + ) ? $var1['key147'] + : $var1['key143']; + break; + case 2: + if (!is_null($var1['key281'])) { + $var1['key277'] + = $var1['key281']; + } + $var1['key143'] = !is_null( + $var1['key151'], + ) ? $var1['key151'] + : $var1['key143']; + break; + case 3: + if (!is_null($var1['key283'])) { + $var1['key277'] + = $var1['key283']; + } + $var1['key143'] = !is_null( + $var1['key155'], + ) ? $var1['key155'] + : $var1['key143']; + break; + case 4: + if (!is_null($var1['key285'])) { + $var1['key277'] + = $var1['key285']; + } + $var1['key143'] = !is_null( + $var1['key159'], + ) ? $var1['key159'] + : $var1['key143']; + break; + case 5: + if (!is_null($var1['key287'])) { + $var1['key277'] + = $var1['key287']; + } + $var1['key143'] = !is_null( + $var1['key163'], + ) ? $var1['key163'] + : $var1['key143']; + break; + case 6: + if (!is_null($var1['key289'])) { + $var1['key277'] + = $var1['key289']; + } + $var1['key143'] = !is_null( + $var1['key167'], + ) ? $var1['key167'] + : $var1['key143']; + break; + case 7: + if (!is_null($var1['key291'])) { + $var1['key277'] + = $var1['key291']; + } + $var1['key143'] = !is_null( + $var1['key171'], + ) ? $var1['key171'] + : $var1['key143']; + break; + } + } +//Tag replacement + $var1['key74'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key74'] ?? '', + ); + $var1['key78'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key78'] ?? '', + ); + $var1['key82'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key82'] ?? '', + ); + $var1['key86'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key86'] ?? '', + ); + $var1['key90'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key90'] ?? '', + ); + $var1['key778'] = str_replace( + [ + '{scLaunchYear}', + '{scLaunchMonthNumeric}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthNumeric}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key760'], + $var1['key761'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key757'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key778'] ?? '', + ); + $var1['key94'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key94'] ?? '', + ); + $var1['key98'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key98'] ?? '', + ); + $var1['key102'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{wkn}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{distributionType}', + '{valoren}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key788'], + $var1['key787'], + $var1['key766'], + $var28, + $var29, + $var1['key789'], + $var1['key790'], + ], + $var1['key102'] ?? '', + ); + $var1['key506'] = str_replace( + [ + '{subfundName}', + '{ISIN}', + '{class}', + '{scCurrency}', + '{distributionType}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key786'], + $var1['key787'], + $var1['key766'], + $var29, + $var1['key790'], + ], + $var1['key506'] ?? '', + ); + $var30 = is_null($var1['key751']) + ? '' + : number_format( + round((float) $var1['key751'], 2), + 2, + ); + $var1['key482'] = str_replace( + [ + '{performanceFee}', + '{performanceBenchmark}', + ], + [ + $var30, + $var1['key749'], + ], + $var1['key482'] ?? '', + ); + $var1['key404'] = str_replace( + [ + '{performanceFee}', + '{performanceBenchmark}', + '{scCurrency}', + '{hedgeType}', + ], + [ + $var30, + $var1['key749'], + $var1['key766'], + $var28, + ], + $var1['key404'] ?? '', + ); + if ($var1['key782'] === null) { + $var31 = null; + $var1['key407'] = null; + } else { + $var31 = number_format( + round((float) $var1['key782'], 2), + 2, + ); + $var1['key407'] = str_replace( + '{pastPerformanceFee}', + $var31, + $var1['key407'] ?? '', + ); + } + $var32 = is_null($var1['key752']) + ? '' + : number_format( + round((float) $var1['key752'], 2), + 2, + ); + $var1['key451'] = str_replace( + '{conversionFeeValue}', + $var32, + $var1['key451'] ?? '', + ); + + $var33 = is_null($var1['key753']) + ? '' + : number_format( + round((float) $var1['key753'], 2), + 2, + ); + + $var34 = is_null($var1['key869']) + ? '' + : number_format( + round((float) $var1['key869'], 2), + 2, + ); + + $var1['key458'] = str_replace( + [ + '{entryChargeValue}', + '{SubscriptionFeeInFavourOfTheFund}', + ], + [ + $var33, + $var34, + ], + $var1['key458'] ?? '', + ); + + $var35 = is_null($var1['key754']) + ? '' + : number_format( + round((float) $var1['key754'], 2), + 2, + ); + + $var36 = is_null($var1['key870']) + ? '' + : number_format( + round((float) $var1['key870'], 2), + 2, + ); + + $var37 = is_null($var1['key871']) + ? '' + : number_format( + round((float) $var1['key871'], 2), + 2, + ); + + $var38 = is_null($var1['key872']) + ? '' + : number_format( + round((float) $var1['key872'], 2), + 2, + ); + + $var1['key462'] = str_replace( + [ + '{exitChargeValue}', + '{RedemptionFeeInFavourOfTheFund}', + '{EffectiveRedemptionFee}', + '{EffectiveSubscriptionFee}', + ], + [ + $var35, + $var36, + $var37, + $var38, + ], + $var1['key462'] ?? '', + ); + + $var1['key466'] = str_replace( + '{conversionFeeValue}', + $var32, + $var1['key466'] ?? '', + ); + + if ($var1['key53'] === null) { + $var1['key671'] = null; + } else { + $var1['key671'] = str_replace( + '{otherShareclasses}', + $var1['key53'], + $var1['key671'] ?? '', + ); + } + if ($var1['key766'] === null || $var1['key767'] === null) { + $var1['key684'] = null; + } else { + $var1['key684'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + ], + [ + $var1['key766'], + $var1['key767'], + ], + $var1['key684'] ?? '', + ); + } + $var39 = [ + '{benchmark}', + '{scCurrency}', + '{hedgeType}', + '{quartileRanking}', + ]; + $var40 = [ + $var1['key523'], + $var1['key766'], + $var28 ?? '', + $var1['key783'], + ]; + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var39[] = "{benchmark$var12}"; + $var40[] = $var1['key523' . $var12]; + } + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var39[] = "{benchmark$var12}"; + $var40[] = $var1['key523' . $var12]; + } + + + $var1['key515'] = str_replace( + $var39, + $var40, + $var1['key515'] ?? '', + ); + + + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var1['key515' . $var12] = str_replace( + $var39, + $var40, + $var1['key515' . $var12] ?? '', + ); + } + + $var1['key519'] = str_replace( + $var39, + $var40, + $var1['key519'] ?? '', + ); + + if ($var1['key760'] === null || $var1['key756'] === null) { + $var1['key551'] = null; + $var1['key555'] = null; + } + $var1['key563'] = str_replace( + [ + '{scCurrency}', + '{class}', + ], + [ + $var1['key766'], + $var1['key787'], + ], + $var1['key563'] ?? '', + ); + + $var1['key541'] = str_replace( + $var39, + $var40, + $var1['key541'] ?? '', + ); + + $var1['key603'] = str_replace( + $var39, + $var40, + $var1['key603'] ?? '', + ); + + $var1['key607'] = str_replace( + $var39, + $var40, + $var1['key607'] ?? '', + ); + + $var1['key611'] = str_replace( + $var39, + $var40, + $var1['key611'] ?? '', + ); + + $var1['key615'] = str_replace( + $var39, + $var40, + $var1['key615'] ?? '', + ); + + $var1['key619'] = str_replace( + $var39, + $var40, + $var1['key619'] ?? '', + ); + + $var1['key623'] = str_replace( + $var39, + $var40, + $var1['key623'] ?? '', + ); + + $var1['key559'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{class}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key787'], + ], + $var1['key559'] ?? '', + ); + $var1['key319'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + ], + [ + $var1['key766'], + $var1['key767'], + ], + $var1['key319'] ?? '', + ); + $var1['key137'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + ], + [ + $var1['key766'], + $var1['key767'], + ], + $var1['key137'] ?? '', + ); + if ( + $var1['key4'] === null + && (str_contains($var1['key277'] ?? '', '{srri}')) + ) { + $var1['key277'] = null; + } else { + $var1['key277'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key277'] ?? '', + ); + } + if ( + $var1['key4'] === null + && (str_contains($var1['key274'] ?? '', '{srri}')) + ) { + $var1['key274'] = null; + } else { + $var1['key274'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key274'] ?? '', + ); + } + if ( + $var1['key4'] === null + && (str_contains($var1['key355'] ?? '', '{srri}')) + ) { + $var1['key355'] = null; + } else { + $var1['key355'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key355'] ?? '', + ); + } + if ( + $var1['key4'] === null + && (str_contains($var1['key358'] ?? '', '{srri}')) + ) { + $var1['key358'] = null; + } else { + $var1['key358'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key358'] ?? '', + ); + } + if ( + $var1['key4'] === null + && (str_contains($var1['key361'] ?? '', '{srri}')) + ) { + $var1['key361'] = null; + } else { + $var1['key361'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key361'] ?? '', + ); + } + $var1['key373'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key373'] ?? '', + ); + $var1['key377'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key377'] ?? '', + ); + $var1['key381'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key381'] ?? '', + ); + $var1['key385'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key385'] ?? '', + ); + $var1['key389'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key389'] ?? '', + ); + $var1['key393'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key393'] ?? '', + ); + $var1['key397'] = str_replace( + '{srri}', + $var1['key4'] ?? '', + $var1['key397'] ?? '', + ); + + unset($var39); + + $var39['key107'] = [ + '{benchmark}', + '{subfundName}', + '{scCurrency}', + '{sfCurrency}', + ]; + + $var39['key147'] + = $var39['key151'] + = $var39['key155'] + = $var39['key159'] + = $var39['key163'] + = $var39['key167'] + = $var39['key171'] = ['{benchmark}']; + + $var39['key113'] + = $var39['key105'] + = $var39['key109'] + = $var39['key111'] + = $var39['key128'] + = $var39['key130'] + = $var39['key133'] + = $var39['key135'] + = $var39['key137'] + = $var39['key140'] + = $var39['key143'] + = $var39['key175'] + = $var39['key179'] + = $var39['key183'] + = $var39['key237'] + = $var39['key241'] + = $var39['key245'] + = $var39['key249'] + = $var39['key253'] + = $var39['key257'] + = $var39['key261'] + = $var39['key186'] + = $var39['key188'] + = $var39['key190'] + = $var39['key192'] + = $var39['key194'] + = $var39['key196'] + = $var39['key198'] + = [ + '{benchmark}', + '{scCurrency}', + '{sfCurrency}', + ]; + + $var39['key123'] = [ + '{benchmark}', + '{scCurrency}', + '{sfCurrency}', + '{subfundName}', + ]; + + $var39['key867'] = [ + '{benchmark}', + '{scCurrency}', + '{sfCurrency}', + '{class}', + ]; + $var39['key125'] + = $var39['key855'] = [ + '{benchmark}', + '{scCurrency}', + '{sfCurrency}', + '{hedgeType}', + ]; + + unset($var40); + $var40['key107'] = [ + $var1['key523'], + $var1['key784'], + $var1['key766'], + $var1['key767'], + ]; + $var40['key113'] + = $var40['key105'] + = $var40['key109'] + = $var40['key111'] + = $var40['key128'] + = $var40['key130'] + = $var40['key133'] + = $var40['key135'] + = $var40['key137'] + = $var40['key140'] + = $var40['key143'] + = $var40['key175'] + = $var40['key179'] + = $var40['key183'] + = $var40['key237'] + = $var40['key241'] + = $var40['key245'] + = $var40['key249'] + = $var40['key253'] + = $var40['key257'] + = $var40['key261'] + = $var40['key186'] + = $var40['key188'] + = $var40['key190'] + = $var40['key192'] + = $var40['key194'] + = $var40['key196'] + = $var40['key198'] + = [ + $var1['key523'], + $var1['key766'], + $var1['key767'], + ]; + + $var40['key123'] = [ + $var1['key523'], + $var1['key766'], + $var1['key767'], + $var1['key784'], + ]; + + $var40['key867'] = [ + $var1['key523'], + $var1['key766'], + $var1['key767'], + $var1['key787'], + ]; + + $var40['key147'] + = $var40['key151'] + = $var40['key155'] + = $var40['key159'] + = $var40['key163'] + = $var40['key167'] + = $var40['key171'] = [ + $var1['key523'], + ]; + + $var40['key125'] + = $var40['key855'] = [ + $var1['key523'], + $var1['key766'], + $var1['key767'], + $var28, + ]; + foreach ($var39 as $var41 => $var42) { + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var39[$var41][] = "{benchmark$var12}"; + $var40[$var41][] = $var1['key523' . $var12]; + } + } + $var1['key107'] = str_replace( + $var39['key107'], + $var40['key107'], + $var1['key107'] ?? '', + ); + $var1['key113'] = str_replace( + $var39['key113'], + $var40['key113'], + $var1['key113'] ?? '', + ); + $var1['key867'] = str_replace( + $var39['key867'], + $var40['key867'], + $var1['key867'] ?? '', + ); + $var1['key123'] = str_replace( + $var39['key123'], + $var40['key123'], + $var1['key123'] ?? '', + ); + $var1['key125'] = str_replace( + $var39['key125'], + $var40['key125'], + $var1['key125'] ?? '', + ); + $var1['key855'] = str_replace( + $var39['key855'], + $var40['key855'], + $var1['key855'] ?? '', + ); + + $var1['key105'] = str_replace( + $var39['key105'], + $var40['key105'], + $var1['key105'] ?? '', + ); + + $var1['key147'] = str_replace( + $var39['key147'], + $var40['key147'], + $var1['key147'] ?? '', + ); + + $var1['key151'] = str_replace( + $var39['key151'], + $var40['key151'], + $var1['key151'] ?? '', + ); + + $var1['key155'] = str_replace( + $var39['key155'], + $var40['key155'], + $var1['key155'] ?? '', + ); + + $var1['key159'] = str_replace( + $var39['key159'], + $var40['key159'], + $var1['key159'] ?? '', + ); + + $var1['key163'] = str_replace( + $var39['key163'], + $var40['key163'], + $var1['key163'] ?? '', + ); + + $var1['key167'] = str_replace( + $var39['key167'], + $var40['key167'], + $var1['key167'] ?? '', + ); + + $var1['key171'] = str_replace( + $var39['key171'], + $var40['key171'], + $var1['key171'] ?? '', + ); + + $var1['key111'] = str_replace( + $var39['key111'], + $var40['key111'], + $var1['key111'] ?? '', + ); + $var1['key135'] = str_replace( + $var39['key135'], + $var40['key135'], + $var1['key135'] ?? '', + ); + $var1['key130'] = str_replace( + $var39['key130'], + $var40['key130'], + $var1['key130'] ?? '', + ); + $var1['key128'] = str_replace( + $var39['key128'], + $var40['key128'], + $var1['key128'] ?? '', + ); + $var1['key133'] = str_replace( + $var39['key133'], + $var40['key133'], + $var1['key133'] ?? '', + ); + $var1['key137'] = str_replace( + $var39['key137'], + $var40['key137'], + $var1['key137'] ?? '', + ); + $var1['key140'] = str_replace( + $var39['key140'], + $var40['key140'], + $var1['key140'] ?? '', + ); + $var1['key143'] = str_replace( + $var39['key143'], + $var40['key143'], + $var1['key143'] ?? '', + ); + $var1['key109'] = str_replace( + $var39['key109'], + $var40['key109'], + $var1['key109'] ?? '', + ); + $var1['key186'] = str_replace( + $var39['key186'], + $var40['key186'], + $var1['key186'] ?? '', + ); + $var1['key188'] = str_replace( + $var39['key188'], + $var40['key188'], + $var1['key188'] ?? '', + ); + $var1['key190'] = str_replace( + $var39['key190'], + $var40['key190'], + $var1['key190'] ?? '', + ); + $var1['key192'] = str_replace( + $var39['key192'], + $var40['key192'], + $var1['key192'] ?? '', + ); + $var1['key194'] = str_replace( + $var39['key194'], + $var40['key194'], + $var1['key194'] ?? '', + ); + $var1['key196'] = str_replace( + $var39['key196'], + $var40['key196'], + $var1['key196'] ?? '', + ); + $var1['key198'] = str_replace( + $var39['key198'], + $var40['key198'], + $var1['key198'] ?? '', + ); + $var1['key237'] = str_replace( + $var39['key237'], + $var40['key237'], + $var1['key237'] ?? '', + ); + $var1['key241'] = str_replace( + $var39['key241'], + $var40['key241'], + $var1['key241'] ?? '', + ); + $var1['key245'] = str_replace( + $var39['key245'], + $var40['key245'], + $var1['key245'] ?? '', + ); + $var1['key249'] = str_replace( + $var39['key249'], + $var40['key249'], + $var1['key249'] ?? '', + ); + $var1['key253'] = str_replace( + $var39['key253'], + $var40['key253'], + $var1['key253'] ?? '', + ); + $var1['key257'] = str_replace( + $var39['key257'], + $var40['key257'], + $var1['key257'] ?? '', + ); + $var1['key261'] = str_replace( + $var39['key261'], + $var40['key261'], + $var1['key261'] ?? '', + ); + $var1['key175'] = str_replace( + $var39['key175'], + $var40['key175'], + $var1['key175'] ?? '', + ); + $var1['key179'] = str_replace( + $var39['key179'], + $var40['key179'], + $var1['key179'] ?? '', + ); + $var1['key183'] = str_replace( + $var39['key183'], + $var40['key183'], + $var1['key183'] ?? '', + ); + + $var1['key547'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + ], + [ + $var1['key766'], + $var1['key767'], + ], + $var1['key547'] ?? '', + ); + $var1['key537'] = str_replace( + [ + '{scLaunchYear}', + '{scLaunchMonthNumeric}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthNumeric}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key760'], + $var1['key761'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key757'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key537'] ?? '', + ); + $var1['key631'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{scLaunchYear}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key760'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key631'] ?? '', + ); + + $var1['key635'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{scLaunchYear}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key760'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key635'] ?? '', + ); + $var1['key639'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{scLaunchYear}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key760'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key639'] ?? '', + ); + $var1['key643'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{scLaunchYear}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key760'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key643'] ?? '', + ); + $var1['key647'] = str_replace( + [ + '{scCurrency}', + '{sfCurrency}', + '{scLaunchYear}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key766'], + $var1['key767'], + $var1['key760'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key758'], + $var1['key759'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key647'] ?? '', + ); + $var1['key653'] = str_replace( + [ + '{fundName}', + '{subfundName}', + ], + [ + $var1['key785'], + $var1['key784'], + ], + $var1['key653'] ?? '', + ); + + $var1['key674'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key674'] ?? '', + ); + $var1['key681'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key681'] ?? '', + ); + $var1['key726'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key726'] ?? '', + ); + $var1['key730'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key730'] ?? '', + ); + $var1['key734'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key734'] ?? '', + ); + $var1['key738'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key738'] ?? '', + ); + $var1['key742'] = str_replace( + '{distributionType}', + $var29 ?? '', + $var1['key742'] ?? '', + ); + + $var1['key714'] = str_replace( + [ + '{class}', + '{distributionType}', + '{fundName}', + '{subfundName}', + ], + [ + $var1['key787'], + $var29, + $var1['key785'], + $var1['key784'], + ], + $var1['key714'] ?? '', + ); + $var1['key718'] = str_replace( + [ + '{class}', + '{distributionType}', + ], + [ + $var1['key787'], + $var29, + ], + $var1['key718'] ?? '', + ); + $var1['key722'] = str_replace( + [ + '{class}', + '{distributionType}', + ], + [ + $var1['key787'], + $var29, + ], + $var1['key722'] ?? '', + ); + + $var1['key567'] = str_replace( + [ + '{subfundName}', + '{fundName}', + '{ISIN}', + '{class}', + '{scCurrency}', + '{hedgeType}', + '{sedol}', + ], + [ + $var1['key784'], + $var1['key785'], + $var1['key786'], + $var1['key787'], + $var1['key766'], + $var28, + $var1['key790'], + ], + $var1['key567'] ?? '', + ); + +//HACK FOR AIF + $var1['key873'] = is_null($var1['key873']) + ? null + : number_format( + round((float) $var1['key873'], 2), + 2, + ); + $var1['key874'] = is_null($var1['key874']) + ? null + : number_format( + round((float) $var1['key874'], 2), + 2, + ); + $var1['key448'] = str_replace( + [ + '{ManagementFee}', + '{EffectiveManagementFee}', + ], + [ + (string) $var1['key873'], + (string) $var1['key874'], + ], + $var1['key448'] ?? '', + ); + unset($var1['key873'], $var1['key874']); +//END HACK FOR AIF + +//End Tag replacement +//Conditional fields +//HEADER + $var1['key875'] = $var1['key769']; +//RRP + $var1['key876'] = trim( + $var1['key274'] . ' ' + . $var1['key277'], + ); +//CF + if ($var1['key753'] === null) { + $var1['key877'] = null; + $var6['key878'] = null; + } elseif ($var1['key753'] == 0 && !is_null($var1['key453'])) { + $var1['key877'] = $var1['key453']; + $var6['key878'] = '0.00'; + } else { + if ( + !is_null($var1['key458']) + && !empty($var1['key458']) + ) { + $var1['key877'] = $var1['key458']; + } else { + $var1['key877'] = number_format( + round((float) $var1['key753'], 2), + 2, + ) . '%'; + } + $var6['key878'] = number_format(round((float) $var1['key753'], 2), 2); + } + if ($var1['key754'] === null) { + $var1['key879'] = null; + $var6['key880'] = null; + } elseif ($var1['key754'] == 0 && !is_null($var1['key453'])) { + $var1['key879'] = $var1['key453']; + $var6['key880'] = '0.00'; + } else { + if ( + !is_null($var1['key462']) + && !empty($var1['key462']) + ) { + $var1['key879'] = $var1['key462']; + } else { + $var1['key879'] = number_format( + round((float) $var1['key754'], 2), + 2, + ) . '%'; + } + $var6['key880'] = number_format(round((float) $var1['key754'], 2), 2); + } + if ($var1['key752'] === null) { + $var1['key881'] = null; + $var6['key882'] = null; + } elseif ($var1['key752'] == 0 && !is_null($var1['key453'])) { + $var1['key881'] = $var1['key453']; + $var6['key882'] = '0.00'; + } else { + if ( + !is_null($var1['key466']) + && !empty($var1['key466']) + ) { + $var1['key881'] = $var1['key466']; + } else { + $var1['key881'] = number_format(round($var1['key752'], 2), 2) . '%'; + } + $var6['key882'] = number_format(round($var1['key752'], 2), 2); + } + if (!is_null($var1['key749']) && floatval($var1['key751']) != 0) { + $var1['key883'] = $var30; + } else { + $var1['key883'] = null; + } + $var1['key407'] = is_null( + $var1['key782'], + ) ? null : $var1['key407']; + if (!is_null($var1['key749']) && floatval($var1['key751']) != 0) { + $var1['key884'] = trim( + $var1['key404'] . ' ' + . $var1['key407'], + ); + } else { + $var1['key884'] + = $var1['key409']; + } + if ( + !is_null($var1['key753']) && $var1['key753'] <> 0 + && !is_null( + $var1['key754'], + ) + && $var1['key754'] <> 0 + ) { + $var1['key412'] + = $var1['key412']; + } elseif ( + !is_null($var1['key753']) && $var1['key753'] <> 0 + && (is_null( + $var1['key754'], + ) + || $var1['key754'] == 0) + ) { + $var1['key412'] + = $var1['key416']; + } elseif ( + (is_null($var1['key753']) || $var1['key753'] == 0) + && !is_null( + $var1['key754'], + ) + && $var1['key754'] <> 0 + ) { + $var1['key412'] + = $var1['key420']; + } else { + $var1['key412'] + = $var1['key424']; + } + $var1['key436'] = (is_null($var1['key751']) + || floatval( + $var1['key751'], + ) == 0) ? null : $var1['key436']; + + $var8['key431'] = $var1['key431']; + $var8['key748'] = $var1['key748']; + + if ($var1['key431'] !== null && $var1['key748'] === null) { + $var1['key428'] + = $var1['key433'] === null + ? $var1['key428'] + : $var1['key433']; + $var1['key748'] = number_format( + round((float) $var1['key431'], 2), + 2, + ) . '%'; + $var6['key885'] = number_format( + round((float) $var1['key431'], 2), + 2, + ); + } else { + $var1['key428'] + = $var1['key428']; + $var1['key748'] = ($var1['key748'] === null) ? null + : number_format(round((float) $var1['key748'], 2), 2) . '%'; + $var6['key885'] = ($var1['key748'] === null) ? null + : number_format(round((float) $var1['key748'], 2), 2); + } + if ( + $var1['key752'] !== null + && ($var1['key752'] != $var1['key753'] + || $var1['key752'] != $var1['key754']) + ) { + $var1['key451'] + = $var1['key451']; + $var1['key455'] = $var1['key455']; + } else { + $var1['key451'] = null; + $var1['key455'] = null; + $var1['key881'] = null; + $var6['key882'] = null; + } + if ($var1['key455'] !== null) { + $var1['key451'] = null; + } +//PP + $var1['key515'] = ($var1['key61'] == 1) + ? $var1['key515'] : null; + $var1['key519'] = ($var1['key518'] == 1) + ? $var1['key519'] : null; + $var1['key502'] = (!is_null( + $var1['key45'], + ) + && $var1['key45'] > 0) + ? $var1['key502'] : null; + + if ( + $var1['key58'] != 'key886' + && !empty($var1['key627']) + ) { + $var1['key551'] + = $var1['key627']; + } else { + if ($var1['key760'] == $var1['key756']) { + $var1['key551'] + = $var1['key551']; + } else { + $var1['key551'] + = $var1['key555']; + } + } + + $var1['key412'] = str_replace( + [ + '{entryChargeValue}', + '{exitChargeValue}', + '{RedemptionFeeInFavourOfTheFund}', + '{SubscriptionFeeInFavourOfTheFund}', + '{EffectiveRedemptionFee}', + '{EffectiveSubscriptionFee}', + ], + [ + $var33, + $var35, + $var36, + $var34, + $var37, + $var38, + ], + $var1['key412'] ?? '', + ); + + $var1['key416'] = str_replace( + [ + '{entryChargeValue}', + '{exitChargeValue}', + '{RedemptionFeeInFavourOfTheFund}', + '{SubscriptionFeeInFavourOfTheFund}', + '{EffectiveRedemptionFee}', + '{EffectiveSubscriptionFee}', + ], + [ + $var33, + $var35, + $var36, + $var34, + $var37, + $var38, + ], + $var1['key416'] ?? '', + ); + + $var1['key420'] = str_replace( + [ + '{entryChargeValue}', + '{exitChargeValue}', + '{RedemptionFeeInFavourOfTheFund}', + '{SubscriptionFeeInFavourOfTheFund}', + '{EffectiveRedemptionFee}', + '{EffectiveSubscriptionFee}', + ], + [ + $var33, + $var35, + $var36, + $var34, + $var37, + $var38, + ], + $var1['key420'] ?? '', + ); + + $var1['key424'] = str_replace( + [ + '{entryChargeValue}', + '{exitChargeValue}', + '{RedemptionFeeInFavourOfTheFund}', + '{SubscriptionFeeInFavourOfTheFund}', + '{EffectiveRedemptionFee}', + '{EffectiveSubscriptionFee}', + ], + [ + $var33, + $var35, + $var36, + $var34, + $var37, + $var38, + ], + $var1['key424'] ?? '', + ); + + $var1['key551'] = str_replace( + [ + '{scLaunchYear}', + '{scLaunchMonthNumeric}', + '{scLaunchMonthTextual}', + '{scLaunchDay}', + '{sfLaunchYear}', + '{sfLaunchMonthNumeric}', + '{sfLaunchMonthTextual}', + '{sfLaunchDay}', + '{class}', + '{scReLaunchYear}', + '{scReLaunchDay}', + '{scReLaunchMonthNumeric}', + '{scReLaunchMonthTextual}', + ], + [ + $var1['key760'], + $var1['key761'], + $var1['key762'], + $var1['key763'], + $var1['key756'], + $var1['key757'], + $var1['key758'], + $var1['key759'], + $var1['key787'], + $var18 ?? '', + $var22 ?? '', + $var19 ?? '', + $var21 ?? '', + ], + $var1['key551'] ?? '', + ); + + if ( + $var1['key766'] <> $var1['key767'] + && !empty($var1['key559']) + ) { + $var1['key563'] + = $var1['key559']; + } + + + unset($var1['key72'], $var1['key518']); + + $var43 = true; + if (!empty($var27)) { + $var1['key68'] = unserialize($var27); + $var44 = []; + for ($var12 = 2; $var12 <= $var11; $var12++) { + $var44 = array_merge( + $var44, + array_column( + $var1['key68'], + 'key887' . $var12, + ), + ); + } + $var45 = array_merge( + array_column($var1['key68'], 'key888'), + array_column($var1['key68'], 'key887'), + $var44, + ); + foreach ($var45 as $var46) { + if (is_numeric($var46)) { + $var43 = false; + break; + } + } + } + + if ($var43 === false) { + $var1['key527'] = null; + + if (!empty($var1['key69'])) { + $var1['key69'] = unserialize( + $var1['key69'], + ); + + if ( + !isset($var1['key69']['key889']) + || !isset($var1['key69']['key890']) + || !isset($var1['key69']['key590']) + ) { + $var1['key578'] = null; + $var1['key590'] = null; + } + + if ( + !isset($var1['key69']['key891']) + || !isset($var1['key69']['key892']) + || !isset($var1['key69']['key593']) + ) { + $var1['key581'] = null; + $var1['key593'] = null; + } + + if ( + !isset($var1['key69']['key893']) + || !isset($var1['key69']['key894']) + || !isset($var1['key69']['key596']) + ) { + $var1['key584'] = null; + $var1['key596'] = null; + } + + if ( + !isset($var1['key69']['key895']) + || !isset($var1['key69']['key896']) + || !isset($var1['key69']['key599']) + ) { + $var1['key587'] = null; + $var1['key599'] = null; + } + } + } else { + $var1['key498'] = null; + $var1['key544'] = null; + $var1['key547'] = null; + $var1['key563'] = null; + $var1['key530'] = null; + $var1['key533'] = null; + $var1['key537'] = null; + $var1['key541'] = null; + $var1['key578'] = null; + $var1['key581'] = null; + $var1['key584'] = null; + $var1['key587'] = null; + $var1['key590'] = null; + $var1['key593'] = null; + $var1['key596'] = null; + $var1['key599'] = null; + } + unset( + $var1['key68'], $var1['key897'], $var1['key898'], $var1['key777'], $var1['key776'], $var1['key69'], $var1['key775'], $var1['key899'], + ); + +//PI + $var1['key671'] = is_null( + $var1['key53'], + ) ? null : $var1['key671']; +//End Conditional fields + $var1['key792'] = $var24->getDate($var1, true); + $var47 = $var24->getTagReferenceDate($var1['key792']); + + if (!empty($var1['key750'])) { + switch ($var1['key750']) { + case 'key900': + $var48 = '{year}-{monthNumeric}-{day}'; + break; + case 'key901': + $var48 = $var2['key902'] ?? null; + break; + case 'key903': + $var48 = $var2['key904'] ?? null; + break; + case 'key905': + $var48 = $var2['key906'] ?? null; + break; + default: + throw new Exception('dateFormat "' . $var1['key750'] . '" doesn\'t exist.'); + } + [ + $var49, + $var50, + $var51, + ] = explode( + '-', + $var1['key792'], + ); + $var52 = date( + 'key755', + strtotime( + $var1['key792'], + ), + ); + $var53 = $var2["MonthName{$var52}"] ?? null; + $var47 = str_replace( + [ + '{year}', + '{monthNumeric}', + '{monthTextual}', + '{day}', + ], + [ + $var49, + $var50, + $var53, + $var51, + ], + $var48, + ); + } + + if (!is_null($var1['key428'])) { + if (is_null($var1['key794'])) { + $var1['key794'] = 0; + } + $var55 = time(); + [ + $var56, + $var57, + ] = explode('-', date('Y-m-d', $var55)); + $var58 = date('key755', $var55); + $var59 = $var2["MonthName{$var58}"] ?? null; + + $var1['key428'] = str_replace( + [ + '{ongoingChargesYear}', + '{ongoingChargesMonth}', + '{ongoingChargesMonthTextual}', + ], + [ + $var56, + $var57, + $var59, + ], + $var1['key428'], + ); + unset($var55, $var56, $var57, $var59); + } + +//This is necessary because str_replace convert null in '' + $var60 = array_keys($var1); + for ($var12 = 0, $var61 = \count($var60); $var12 < $var61; $var12++) { + if ($var1[$var60[$var12]] === '') { + $var1[$var60[$var12]] = null; + } + } +//META + $var62 = implode(',', $var4->getEmailGroupOther('key908')); + $var7['key909'] = 'Version=2.0; GeneratorContact=' . $var62 . '; DocumentType=KID; '; + if (isset($var1['key9'])) { + $var7['key909'] .= 'PublicationCountry=' . $var1['key9'] + . (($var1['key910'] == 'key513' + && $var1['key9'] == 'key441' + && $var1['key6'] != 'key441') ? '.QFI' + : '') . '; '; + } + unset($var1['key910'], $var1['key6']); + if (isset($var1['key10'])) { + $var7['key909'] .= 'Language=' . $var1['key10'] . '; '; + } + $var7['key909'] .= 'RepShareClass=' . $var1['key786'] . '; RepShareClassCurrency=' + . $var1['key766'] . '; ShareClass=' . $var1['key786']; + if (isset($var1['key789'])) { + $var7['key909'] .= ',VALOR:' . $var1['key789']; + } + if (isset($var1['key788'])) { + $var7['key909'] .= ',WKNDE:' . $var1['key788']; + } + $var7['key909'] .= '; '; + if (isset($var1['key15']) && $var1['key15'] == 'key911') { + $var7['key909'] .= 'DateOfPublication=' + . $var1['key792'] + . '; RecordDate=' + . $var1['key792'] + . '; ModificationDate=' + . $var1['key792'] + . '; '; + if ( + (array_key_exists('key912', $var1) + && $var1['key912'] != 1) + || !array_key_exists('key912', $var1) + ) { + $var7['key909'] .= 'DocumentUrl=' + . $var1['key786'] + . '/' + . $var1['key10'] + . 'key913' + . $var1['key9'] + . '; '; + } + } + unset($var1['key912']); + if (isset($var1['key4'])) { + $var7['key909'] .= 'SRRI=' . $var1['key4'] . '; '; + } + $var7['key909'] .= 'PerformanceFee=' . $var30 . '%; '; + if (isset($var1['key877'])) { + $var7['key909'] .= 'EntryCharge=' . $var1['key877'] . '; '; + } + if (isset($var1['key879'])) { + $var7['key909'] .= 'ExitCharge=' . $var1['key879'] . '; '; + } + if (isset($var1['key748'])) { + $var7['key909'] .= 'OngoingCharges=' . $var1['key748'] . '; '; + } + $var7['key909'] .= 'ProductionDateTime=' . $var24->moteurData['key914'] . ';'; + if (isset($var1['key9'])) { + $var7['key9'] = $var1['key9']; + $var8['key9'] = $var1['key9']; + } + if (isset($var1['key10'])) { + $var7['key10'] = $var1['key10']; + $var8['key10'] = $var1['key10']; + } + if (isset($var1['key14'])) { + $var7['key14'] = $var1['key14']; + $var8['key14'] = $var1['key14']; + } + $var8['key746'] = $var1['key746']; + $var8['key747'] = $var1['key747']; +//Data for bot + if ($var3 == 'key2') { + $var6['key67'] = $var1['key67']; + $var6['key746'] = $var1['key746']; + $var6['key747'] = $var1['key747']; + $var6['key915'] = (!isset($var1['key7'])) ? null + : $var1['key7']; + $var6['key792'] = $var1['key792']; + $var6['key16'] = $var1['key16']; + $var6['key916'] = $var1['key786']; + $var6['key917'] = $var1['key785']; + $var6['key918'] = $var1['key784']; + $var6['key919'] = $var1['key787']; + $var6['key920'] = $var1['key766']; + $var6['key921'] = $var1['key4']; + if (!is_null($var1['key749']) && floatval($var1['key751']) != 0) { + $var6['key922'] = $var31; + $var6['key923'] = $var30; + } else { + $var6['key922'] = null; + $var6['key923'] = null; + } + $var6['key924'] = $var7['key909']; + $var6['key925'] = $var1['key745']; + $var6['key926'] = $var1['key791']; + $var6['key8'] + = isset($var1['key8']) + ? $var1['key8'] : null; + + $var63 = $var2; +//This assignment must be done before the replacement tag + $var63['key927'] = $var2['key927'] ?? null; + } + +//Labels + if (!is_null($var1['key928'])) { + $var2['key929'] = $var1['key928']; + } elseif (!is_null($var1['key930'])) { + $var2['key929'] = $var1['key930']; + } + if (!is_null($var1['key931'])) { + $var2['key932'] = $var1['key931']; + } elseif (!is_null($var1['key933'])) { + $var2['key932'] = $var1['key933']; + } + if (!is_null($var1['key934'])) { + $var2['key935'] = $var1['key934']; + } elseif (!is_null($var1['key936'])) { + $var2['key935'] = $var1['key936']; + } + if (!is_null($var1['key937'])) { + $var2['key938'] = $var1['key937']; + } elseif (!is_null($var1['key939'])) { + $var2['key938'] = $var1['key939']; + } + if (!is_null($var1['key940'])) { + $var2['key941'] = $var1['key940']; + } elseif (!is_null($var1['key942'])) { + $var2['key941'] = $var1['key942']; + } + if (!is_null($var1['key943'])) { + $var2['key944'] = $var1['key943']; + } elseif (!is_null($var1['key945'])) { + $var2['key944'] = $var1['key945']; + } + if (!is_null($var1['key946'])) { + $var2['key947'] = $var1['key946']; + } elseif (!is_null($var1['key948'])) { + $var2['key947'] = $var1['key948']; + } + if (!is_null($var1['key949'])) { + $var2['key950'] = $var1['key949']; + } elseif (!is_null($var1['key951'])) { + $var2['key950'] = $var1['key951']; + } + if (!is_null($var1['key952'])) { + $var2['key953'] = $var1['key952']; + } elseif (!is_null($var1['key954'])) { + $var2['key953'] = $var1['key954']; + } + if (!is_null($var1['key955'])) { + $var2['key956'] = $var1['key955']; + } elseif (!is_null($var1['key957'])) { + $var2['key956'] = $var1['key957']; + } + if (!is_null($var1['key958'])) { + $var2['key959'] + = $var1['key958']; + } elseif (!is_null($var1['key960'])) { + $var2['key959'] + = $var1['key960']; + } + if (!is_null($var1['key961'])) { + $var2['key962'] = $var1['key961']; + } elseif (!is_null($var1['key963'])) { + $var2['key962'] = $var1['key963']; + } + if (!is_null($var1['key964'])) { + $var2['key748'] = $var1['key964']; + } elseif (!is_null($var1['key965'])) { + $var2['key748'] = $var1['key965']; + } + if (!is_null($var1['key966'])) { + $var2['key967'] + = $var1['key966']; + } elseif (!is_null($var1['key968'])) { + $var2['key967'] + = $var1['key968']; + } + if (!is_null($var1['key969'])) { + $var2['key883'] = $var1['key969']; + } elseif (!is_null($var1['key970'])) { + $var2['key883'] = $var1['key970']; + } + if (!is_null($var1['key971'])) { + $var2['key927'] + = $var1['key971']; + } elseif (!is_null($var1['key972'])) { + $var2['key927'] + = $var1['key972']; + } + if (!empty($var2['key927'])) { + $var2['key927'] = str_replace( + '{publicationDate}', + $var47, + $var2['key927'], + ); + } + + if (!empty($var1['key494'])) { + if ($var3 == 'key2')//HACK for add ongoincharges in checksum + { + $var63['key748'] = str_replace( + '{ongoingCharges}', + $var1['key748'], + $var1['key494'], + ); + } + $var1['key748'] = $var1['key494']; + } elseif (!empty($var1['key748'])) { + if ($var3 == 'key2')//HACK for add ongoincharges in checksum + { + $var63['key748'] = $var1['key748']; + } + $var1['key748'] = '{ongoingCharges}'; + } + +//convert string to numeric for template + $var1['key751'] += 0; + +//delete unused data + unset( + $var1['key67'], $var1['key746'], $var1['key9'], $var1['key10'], $var1['key14'], $var1['key15'], $var1['key7'], $var1['key749'], $var1['key973'], $var1['key974'], $var1['key975'], $var1['key976'], $var1['key977'], $var1['key978'], $var1['key979'], $var1['key980'], $var1['key981'], $var1['key982'], $var1['key783'], $var1['key431'], $var1['key782'], $var1['key753'], $var1['key754'], $var1['key870'], $var1['key869'], $var1['key871'], $var1['key872'], $var1['key752'], $var1['key791'], $var1['key745'], $var1['key45'], $var1['key756'], $var1['key757'], $var1['key758'], $var1['key759'], $var1['key760'], $var1['key764'], $var1['key765'], $var1['key761'], $var1['key762'], $var1['key763'], $var1['key766'], $var1['key767'], $var1['key785'], $var1['key784'], $var1['key523'], $var1['key53'], $var1['key787'], $var1['key788'], $var1['key789'], $var1['key790'], $var1['key8'], $var1['key792'], $var1['key793'], $var1['key794'], $var1['key16'], $var1['key769'], $var1['key200'], $var1['key203'], $var1['key206'], $var1['key209'], $var1['key212'], $var1['key215'], $var1['key218'], $var1['key221'], $var1['key224'], $var1['key227'], $var1['key230'], $var1['key233'], $var1['key494'], $var1['key750'], $var1['key116'], $var1['key120'], $var1['key147'], $var1['key151'], $var1['key155'], $var1['key159'], $var1['key163'], $var1['key167'], $var1['key171'], $var1['key274'], $var1['key277'], $var1['key279'], $var1['key281'], $var1['key283'], $var1['key285'], $var1['key287'], $var1['key289'], $var1['key291'], $var1['key453'], $var1['key404'], $var1['key407'], $var1['key409'], $var1['key416'], $var1['key420'], $var1['key424'], $var1['key433'], $var1['key555'], $var1['key559'], $var1['key627'], $var1['key458'], $var1['key462'], $var1['key466'], $var1['key795'], $var1['key797'], $var1['key799'], $var1['key801'], $var1['key803'], $var1['key805'], $var1['key807'], $var1['key809'], $var1['key811'], $var1['key813'], $var1['key815'], $var1['key817'], $var1['key819'], $var1['key821'], $var1['key930'], $var1['key928'], $var1['key933'], $var1['key931'], $var1['key936'], $var1['key934'], $var1['key939'], $var1['key937'], $var1['key942'], $var1['key940'], $var1['key945'], $var1['key943'], $var1['key948'], $var1['key946'], $var1['key951'], $var1['key949'], $var1['key954'], $var1['key952'], $var1['key957'], $var1['key955'], $var1['key960'], $var1['key958'], $var1['key963'], $var1['key961'], $var1['key965'], $var1['key964'], $var1['key968'], $var1['key966'], $var1['key970'], $var1['key969'], $var1['key972'], $var1['key971'], $var1['key42'], $var1['key58'], $var1['key983'], $var2['key902'], $var2['key904'], $var2['key906'], $var2['key984'], $var2['key985'], $var2['key986'], $var2['key987'], $var2['key988'], $var2['key989'], $var2['key990'], $var2['key991'], $var2['key992'], $var2['key993'], $var2['key994'], $var2['key995'], + ); +//End Final array construction + + foreach ($var1 as &$var64) { + if ($var64 !== null && trim($var64) === '{empty}') { + $var64 = null; + } + } + + $var65 = [ + 'key996' => [ + 'key74' => $var1['key74'], + 'key78' => $var1['key78'], + 'key82' => $var1['key82'], + 'key86' => $var1['key86'], + 'key90' => $var1['key90'], + 'key875' => $var1['key875'], + ], + 'key997' => [ + 'key94' => $var1['key94'], + 'key98' => $var1['key98'], + 'key102' => $var1['key102'], + 'key778' => $var1['key778'], + 'key780' => $var1['key780'], + ], + 'key929' => [ + 'key105' => $var1['key105'], + 'key107' => $var1['key107'], + 'key109' => $var1['key109'], + 'key111' => $var1['key111'], + 'key113' => $var1['key113'], + 'key867' => $var1['key867'], + 'key123' => $var1['key123'], + 'key125' => $var1['key125'], + 'key128' => $var1['key128'], + 'key130' => $var1['key130'], + 'key133' => $var1['key133'], + 'key135' => $var1['key135'], + 'key137' => $var1['key137'], + 'key140' => $var1['key140'], + 'key143' => $var1['key143'], + 'key175' => $var1['key175'], + 'key179' => $var1['key179'], + 'key183' => $var1['key183'], + 'key855' => $var1['key855'], + 'key237' => $var1['key237'], + 'key241' => $var1['key241'], + 'key245' => $var1['key245'], + 'key249' => $var1['key249'], + 'key253' => $var1['key253'], + 'key257' => $var1['key257'], + 'key261' => $var1['key261'], + 'key186' => $var1['key186'], + 'key188' => $var1['key188'], + 'key190' => $var1['key190'], + 'key192' => $var1['key192'], + 'key194' => $var1['key194'], + 'key196' => $var1['key196'], + 'key198' => $var1['key198'], + ], + 'key932' => [ + 'key4' => $var1['key4'], + 'key265' => $var1['key265'], + 'key268' => $var1['key268'], + 'key270' => $var1['key270'], + 'key272' => $var1['key272'], + 'key876' => $var1['key876'], + 'key293' => $var1['key293'], + 'key297' => $var1['key297'], + 'key322' => $var1['key322'], + 'key325' => $var1['key325'], + 'key328' => $var1['key328'], + 'key331' => $var1['key331'], + 'key334' => $var1['key334'], + 'key337' => $var1['key337'], + 'key340' => $var1['key340'], + 'key343' => $var1['key343'], + 'key346' => $var1['key346'], + 'key349' => $var1['key349'], + 'key300' => $var1['key300'], + 'key352' => $var1['key352'], + 'key355' => $var1['key355'], + 'key358' => $var1['key358'], + 'key361' => $var1['key361'], + 'key302' => $var1['key302'], + 'key304' => $var1['key304'], + 'key306' => $var1['key306'], + 'key308' => $var1['key308'], + 'key310' => $var1['key310'], + 'key313' => $var1['key313'], + 'key316' => $var1['key316'], + 'key319' => $var1['key319'], + 'key370' => $var1['key370'], + 'key366' => $var1['key366'], + 'key368' => $var1['key368'], + 'key364' => $var1['key364'], + 'key373' => $var1['key373'], + 'key377' => $var1['key377'], + 'key381' => $var1['key381'], + 'key385' => $var1['key385'], + 'key389' => $var1['key389'], + 'key393' => $var1['key393'], + 'key397' => $var1['key397'], + ], + 'key935' => [ + 'key881' => $var1['key881'], + 'key748' => $var1['key748'], + 'key879' => $var1['key879'], + 'key877' => $var1['key877'], + 'key883' => $var1['key883'], + 'key401' => $var1['key401'], + 'key884' => $var1['key884'], + 'key412' => $var1['key412'], + 'key428' => $var1['key428'], + 'key436' => $var1['key436'], + 'key438' => $var1['key438'], + 'key443' => $var1['key443'], + 'key448' => $var1['key448'], + 'key451' => $var1['key451'], + 'key455' => $var1['key455'], + 'key470' => $var1['key470'], + 'key474' => $var1['key474'], + 'key478' => $var1['key478'], + 'key482' => $var1['key482'], + 'key486' => $var1['key486'], + 'key490' => $var1['key490'], + ], + 'key938' => [ + 'key774' => $var1['key774'], + 'key590' => $var1['key590'], + 'key593' => $var1['key593'], + 'key596' => $var1['key596'], + 'key599' => $var1['key599'], + 'key498' => $var1['key498'], + 'key506' => $var1['key506'], + 'key515' => $var1['key515'], + 'key510' => $var1['key510'], + 'key502' => $var1['key502'], + 'key527' => $var1['key527'], + 'key530' => $var1['key530'], + 'key533' => $var1['key533'], + 'key537' => $var1['key537'], + 'key541' => $var1['key541'], + 'key544' => $var1['key544'], + 'key547' => $var1['key547'], + 'key551' => $var1['key551'], + 'key563' => $var1['key563'], + 'key578' => $var1['key578'], + 'key581' => $var1['key581'], + 'key584' => $var1['key584'], + 'key587' => $var1['key587'], + 'key603' => $var1['key603'], + 'key607' => $var1['key607'], + 'key611' => $var1['key611'], + 'key615' => $var1['key615'], + 'key619' => $var1['key619'], + 'key623' => $var1['key623'], + 'key631' => $var1['key631'], + 'key635' => $var1['key635'], + 'key639' => $var1['key639'], + 'key643' => $var1['key643'], + 'key647' => $var1['key647'], + 'key567' => $var1['key567'], + 'key571' => $var1['key571'], + 'key575' => $var1['key575'], + ], + 'key941' => [ + 'key684' => $var1['key684'], + 'key650' => $var1['key650'], + 'key659' => $var1['key659'], + 'key662' => $var1['key662'], + 'key665' => $var1['key665'], + 'key668' => $var1['key668'], + 'key671' => $var1['key671'], + 'key674' => $var1['key674'], + 'key681' => $var1['key681'], + 'key726' => $var1['key726'], + 'key730' => $var1['key730'], + 'key734' => $var1['key734'], + 'key738' => $var1['key738'], + 'key742' => $var1['key742'], + 'key714' => $var1['key714'], + 'key718' => $var1['key718'], + 'key722' => $var1['key722'], + 'key689' => $var1['key689'], + 'key692' => $var1['key692'], + 'key695' => $var1['key695'], + 'key698' => $var1['key698'], + 'key701' => $var1['key701'], + 'key704' => $var1['key704'], + 'key707' => $var1['key707'], + 'key710' => $var1['key710'], + ], + 'key998' => [ + 'key751' => $var1['key751'], + 'key786' => $var1['key786'], + 'key61' => $var1['key61'], + ], + ]; + + for ($var12 = 2; $var12 <= $var11; $var12++) { + if (isset($var1['key515' . $var12])) { + $var66['key515' . $var12] + = $var1['key515' . $var12]; + } elseif (is_null($var1['key515' . $var12])) { + unset($var1['key515' . $var12]); + } + } + if (isset($var66)) { + $var65['key938'] = array_merge( + $var65['key938'], + $var66, + ); + } + + if (isset($var1['key999'])) { + $var65['key938']['key999'] = $var1['key999']; + } + if (isset($var1['key519'])) { + $var65['key938']['key519'] + = $var1['key519']; + } else { + unset($var1['key519']); + } + if (isset($var1['key1000'])) { + $var65['key997']['key1000'] = $var1['key1000']; + } else { + unset($var1['key1000']); + } + if (isset($var1['key687'])) { + $var65['key941']['key687'] + = $var1['key687']; + } else { + unset($var1['key687']); + } + if (isset($var1['key653'])) { + $var65['key941']['key653'] + = $var1['key653']; + } else { + unset($var1['key653']); + } +//HACK for structured template + if ($var1['key63'] == 'Structured Products') { + $var67 = new backend(); + $var68 = $var67->getStructuredNarrative($var1['key747']); + + if (!empty(array_intersect_key($var68['key1001'], $var1))) { + throw new Exception('some keys are present in the 2 documentData arrays.'); + } elseif ( + !empty( + array_intersect_key( + $var68['key1002'], + $var2, + ) + ) + ) { + throw new Exception('some keys are present in the 2 templateLabels arrays.'); + } else { + $var1 = array_merge($var1, $var68['key1001']); + $var2 = array_merge($var2, $var68['key1002']); + $var8['key1003'] = $var68['key1003']; + $var65['key1004'] = $var68['key1001']; + unset($var68); + } + } + unset($var1['key63'], $var1['key747']); +//END HACK for structured template + $var69 = []; + foreach ($var65 as $var70 => $var71) { + $var69 = array_merge($var69, $var71); + } + + if ($var69 != $var1) { + $var72 = array_diff_assoc($var69, $var1); + $var73 = array_diff_assoc($var1, $var69); + $var74 = ''; + if (!empty($var72)) { + ob_start(); + print_r(filter_var_array($var72, FILTER_UNSAFE_RAW)); + $var74 .= "

Difference between structured array and data:
" + . ob_get_contents(); + ob_end_clean(); + } + if (!empty($var73)) { + ob_start(); + print_r(filter_var_array($var73, FILTER_UNSAFE_RAW)); + $var74 .= "

Difference between data and structured array:
" + . ob_get_contents(); + ob_end_clean(); + } + throw new Exception( + $var1['key786'] + . $var74, + ); + } + } + + if ($var3 == 'key2') { + return [ + 'key1005' => $var5, + 'key1006' => $var6, + 'key1001' => $var65 ?? null, + 'key1002' => $var2, + 'key1007' => $var63 ?? null, + 'key1008' => $var7, + 'key1009' => $var8, + 'key1010' => $var9, + ]; + } elseif ($var3 == 'key3') { + return $var1; + } else { + return [ + 'key1001' => $var65 ?? null, + 'key1002' => $var2, + 'key1008' => $var7, + 'key1009' => $var8, + ]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-13424.php b/tests/PHPStan/Analyser/data/bug-13424.php new file mode 100644 index 0000000000..051afacae6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13424.php @@ -0,0 +1,17 @@ + 'b', + ); + } else { + $hello = new Hello( $args ); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-13492.php b/tests/PHPStan/Analyser/data/bug-13492.php new file mode 100644 index 0000000000..68a4654032 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13492.php @@ -0,0 +1,57 @@ +getService(); + } + + public function getService(): CustomerService + { + return new CustomerService(); + } + +} + +class CustomerService +{ + + public function create(): Customer + { + return new Customer(); + } + +} + +/** + * @property null|(object{address?: (object{city: null|string, country: null|string, line1: null|string, line2: null|string, postal_code: null|string, state: null|string}&StripeObject), carrier?: null|string, name?: string, phone?: null|string, tracking_number?: null|string}&StripeObject) $shipping Mailing and shipping address for the customer. Appears on invoices emailed to this customer. + * @property null|(object{city: null|string, country: null|string, line1: null|string, line2: null|string, postal_code: null|string, state: null|string}&StripeObject) $address The customer's address. + */ +class Customer extends StripeObject +{ + +} + +class StripeObject +{ + /** @return mixed */ + public function &__get(string $k) + { + + } +} + +function (): void { + $stripe = new StripeClient(); + $customer = $stripe->customers->create(); +}; diff --git a/tests/PHPStan/Analyser/data/bug-13507.php b/tests/PHPStan/Analyser/data/bug-13507.php new file mode 100644 index 0000000000..a28938c31a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13507.php @@ -0,0 +1,15 @@ + $value, + ]; +} diff --git a/tests/PHPStan/Analyser/data/bug-13529.php b/tests/PHPStan/Analyser/data/bug-13529.php new file mode 100644 index 0000000000..a466ef8c35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13529.php @@ -0,0 +1,10 @@ +foo) || !isset($tmp->bar)) { + } +} 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-1861.php b/tests/PHPStan/Analyser/data/bug-1861.php deleted file mode 100644 index 0f00af5b8a..0000000000 --- a/tests/PHPStan/Analyser/data/bug-1861.php +++ /dev/null @@ -1,29 +0,0 @@ -children)) { - case 0: - assertType('array()', $this->children); - break; - case 1: - assertType('array<' . self::class . '>&nonEmpty', $this->children); - assertType(self::class, reset($this->children)); - break; - default: - assertType('array<' . self::class . '>&nonEmpty', $this->children); - break; - } - } -} diff --git a/tests/PHPStan/Analyser/data/bug-2001.php b/tests/PHPStan/Analyser/data/bug-2001.php deleted file mode 100644 index 01ff5a4113..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2001.php +++ /dev/null @@ -1,51 +0,0 @@ - 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-2142.php b/tests/PHPStan/Analyser/data/bug-2142.php deleted file mode 100644 index 9834c2dc28..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2142.php +++ /dev/null @@ -1,99 +0,0 @@ - 0) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo2(array $arr): void - { - if (count($arr) != 0) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo3(array $arr): void - { - if (count($arr) == 1) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo4(array $arr): void - { - if ($arr != []) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo5(array $arr): void - { - if (sizeof($arr) !== 0) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo6(array $arr): void - { - if (count($arr) !== 0) { - assertType('array&nonEmpty', $arr); - } - } - - - /** - * @param string[] $arr - */ - function doFoo7(array $arr): void - { - if (!empty($arr)) { - assertType('array&nonEmpty', $arr); - } - } - - /** - * @param string[] $arr - */ - function doFoo8(array $arr): void - { - if (count($arr) === 1) { - assertType('array&nonEmpty', $arr); - } - } - - - /** - * @param string[] $arr - */ - function doFoo9(array $arr): void - { - if ($arr !== []) { - assertType('array&nonEmpty', $arr); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-2232.php b/tests/PHPStan/Analyser/data/bug-2232.php deleted file mode 100644 index 3f1613c725..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-2378.php b/tests/PHPStan/Analyser/data/bug-2378.php deleted file mode 100644 index ab58f7a28c..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2378.php +++ /dev/null @@ -1,23 +0,0 @@ -', range($s, $s)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-2899.php b/tests/PHPStan/Analyser/data/bug-2899.php deleted file mode 100644 index 2342fb2466..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2899.php +++ /dev/null @@ -1,18 +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-3133.php b/tests/PHPStan/Analyser/data/bug-3133.php deleted file mode 100644 index ea003461fa..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3133.php +++ /dev/null @@ -1,58 +0,0 @@ -|string', $arg); - return; - } - - assertType('string&numeric', $arg); - } - - /** - * @param string|bool|float|int|mixed[]|null $arg - */ - public function doBar($arg): void - { - if (\is_numeric($arg)) { - assertType('float|int|(string&numeric)', $arg); - } - } - - /** - * @param numeric $numeric - * @param numeric-string $numericString - */ - public function doBaz( - $numeric, - string $numericString - ) - { - assertType('float|int|(string&numeric)', $numeric); - assertType('string&numeric', $numericString); - } - - /** - * @param numeric-string $numericString - */ - public function doLorem( - string $numericString - ) - { - $a = []; - $a[$numericString] = 'foo'; - assertType('array&nonEmpty', $a); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3276.php b/tests/PHPStan/Analyser/data/bug-3276.php deleted file mode 100644 index dccb79c5fc..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3276.php +++ /dev/null @@ -1,28 +0,0 @@ -= 7.4 - -namespace Bug3276; - -use function PHPStan\Testing\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-3336.php b/tests/PHPStan/Analyser/data/bug-3336.php deleted file mode 100644 index a0f7127237..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3336.php +++ /dev/null @@ -1,10 +0,0 @@ -', 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/data/bug-3382.php b/tests/PHPStan/Analyser/data/bug-3382.php deleted file mode 100644 index 973b489f4c..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3382.php +++ /dev/null @@ -1,9 +0,0 @@ - 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(0 => 1, ?1 => array(1, 2), ?2 => array(1, 2), ?3 => array(1, 2))', $idGroups); - } -}; diff --git a/tests/PHPStan/Analyser/data/bug-3865.php b/tests/PHPStan/Analyser/data/bug-3865.php new file mode 100644 index 0000000000..c125dc2193 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3865.php @@ -0,0 +1,17 @@ + + */ +class RecursiveClass extends EntityRepository +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-3961-php8.php b/tests/PHPStan/Analyser/data/bug-3961-php8.php deleted file mode 100644 index 657fdf22f6..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3961-php8.php +++ /dev/null @@ -1,21 +0,0 @@ -&nonEmpty', explode('.', $v)); - assertType('*NEVER*', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('array&nonEmpty', explode('.', $v, 0)); - assertType('array&nonEmpty', explode('.', $v, 1)); - assertType('array&nonEmpty', explode($d, $v)); - assertType('array&nonEmpty', explode($m, $v)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3961.php b/tests/PHPStan/Analyser/data/bug-3961.php deleted file mode 100644 index e17706794d..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3961.php +++ /dev/null @@ -1,21 +0,0 @@ -&nonEmpty', explode('.', $v)); - assertType('false', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('array&nonEmpty', explode('.', $v, 0)); - assertType('array&nonEmpty', explode('.', $v, 1)); - assertType('(array&nonEmpty)|false', explode($d, $v)); - assertType('((array&nonEmpty)|false)', explode($m, $v)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3981.php b/tests/PHPStan/Analyser/data/bug-3981.php deleted file mode 100644 index 2a1929cf1a..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3981.php +++ /dev/null @@ -1,25 +0,0 @@ - $a - */ - public function doFoo(array $a): void - { - assertType('array', $a); - $a[] = 2; - assertType('array&nonEmpty', $a); - - unset($a[0]); - assertType('array', $a); - } - - /** - * @param array $a - */ - public function doBar(array $a): void - { - assertType('array', $a); - $a[1] = 2; - assertType('array&nonEmpty', $a); - - unset($a[1]); - assertType('array', $a); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-4091.php b/tests/PHPStan/Analyser/data/bug-4091.php deleted file mode 100644 index 0361c4eb4e..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4091.php +++ /dev/null @@ -1,10 +0,0 @@ - 3) { - echo 'Fizz'; - assertType('int', mt_rand(0,10)); -} diff --git a/tests/PHPStan/Analyser/data/bug-4099.php b/tests/PHPStan/Analyser/data/bug-4099.php deleted file mode 100644 index 571dfb3476..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4099.php +++ /dev/null @@ -1,41 +0,0 @@ - array(\'inner\' => mixed))', $arr); - assertNativeType('array', $arr); - - if (!array_key_exists('key', $arr)) { - assertType('*NEVER*', $arr); - assertNativeType('array', $arr); - throw new \Exception('no key "key" found.'); - } - assertType('array(\'key\' => array(\'inner\' => mixed))', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); - assertType('array(\'inner\' => mixed)', $arr['key']); - assertNativeType('mixed', $arr['key']); - - if (!array_key_exists('inner', $arr['key'])) { - assertType('array(\'key\' => *NEVER*)', $arr); - //assertNativeType('array(\'key\' => mixed)', $arr); - assertType('*NEVER*', $arr['key']); - //assertNativeType('mixed', $arr['key']); - throw new \Exception('need key.inner'); - } - - assertType('array(\'key\' => array(\'inner\' => mixed))', $arr); - assertNativeType('array(\'key\' => array(\'inner\' => mixed))', $arr); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-4207.php b/tests/PHPStan/Analyser/data/bug-4207.php deleted file mode 100644 index e8a1de5f02..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4207.php +++ /dev/null @@ -1,9 +0,0 @@ -&nonEmpty', range(1, 10000)); -}; diff --git a/tests/PHPStan/Analyser/data/bug-4308.php b/tests/PHPStan/Analyser/data/bug-4308.php new file mode 100644 index 0000000000..9584ebaf12 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4308.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug4308; + +class Test +{ + /** + * @var (string|int|null)[] + * @phpstan-var array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } + */ + protected array $updateData = []; + + /** + * @phpstan-param array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } $data + */ + public function update(array $data): void + { + $this->updateData = $data + $this->updateData; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4398.php b/tests/PHPStan/Analyser/data/bug-4398.php deleted file mode 100644 index 23bab3eaa2..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4398.php +++ /dev/null @@ -1,18 +0,0 @@ -&nonEmpty', array_keys($meters)); - assertType('array&nonEmpty', array_values($meters)); -}; diff --git a/tests/PHPStan/Analyser/data/bug-4434.php b/tests/PHPStan/Analyser/data/bug-4434.php deleted file mode 100644 index b60b751ed1..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4434.php +++ /dev/null @@ -1,42 +0,0 @@ -|int<8, max>', PHP_MAJOR_VERSION); - assertType('int|int<8, max>', \PHP_MAJOR_VERSION); - } - } - } -} - -class HelloWorld2 -{ - public function testSendEmailToLog(): void - { - foreach ([1] as $emailFile) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); - if (PHP_MAJOR_VERSION === 100) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); - } else { - assertType('int|int<101, max>', PHP_MAJOR_VERSION); - assertType('int|int<101, max>', \PHP_MAJOR_VERSION); - } - } - } -} diff --git a/tests/PHPStan/Analyser/data/bug-4587.php b/tests/PHPStan/Analyser/data/bug-4587.php deleted file mode 100644 index 880aef8d47..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4587.php +++ /dev/null @@ -1,37 +0,0 @@ - $results */ - $results = []; - - $type = array_map(static function (array $result): array { - assertType('array(\'a\' => int)', $result); - return $result; - }, $results); - - assertType('array int)>', $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\' => string&numeric)', $result); - - return $result; - }, $results); - - assertType('array string&numeric)>', $type); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-4606.php b/tests/PHPStan/Analyser/data/bug-4606.php deleted file mode 100644 index d2d6a3ab45..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4606.php +++ /dev/null @@ -1,23 +0,0 @@ - $assigned - */ - -assertType(Foo::class, $this); -assertType('array', $assigned); - - -/** - * @var array - * @phpstan-var array{\stdClass, int} - */ -$foo = doFoo(); - -assertType('array(stdClass, int)', $foo); diff --git a/tests/PHPStan/Analyser/data/bug-4657.php b/tests/PHPStan/Analyser/data/bug-4657.php deleted file mode 100644 index 3cc65c9ed9..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4657.php +++ /dev/null @@ -1,22 +0,0 @@ -', $count); - - $a = []; - if (isset($array['a'])) $a[] = $array['a']; - if (isset($array['b'])) $a[] = $array['b']; - if (isset($array['c'])) $a[] = $array['c']; - if (isset($array['d'])) $a[] = $array['d']; - if (isset($array['e'])) $a[] = $array['e']; - if (count($a) >= $count) { - assertType('1|2|3|4|5', count($a)); - assertType('array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); - } else { - assertType('0|1|2|3|4|5', count($a)); - assertType('array()|array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); - } -}; - -function(array $array, int $count): void { - if ($count < 1) { - return; - } - - assertType('int<1, max>', $count); - - $a = []; - if (isset($array['a'])) $a[] = $array['a']; - if (isset($array['b'])) $a[] = $array['b']; - if (isset($array['c'])) $a[] = $array['c']; - if (isset($array['d'])) $a[] = $array['d']; - if (isset($array['e'])) $a[] = $array['e']; - if (count($a) > $count) { - assertType('2|3|4|5', count($a)); - assertType('array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); - } else { - assertType('0|1|2|3|4|5', count($a)); - assertType('array()|array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); - } -}; diff --git a/tests/PHPStan/Analyser/data/bug-4711.php b/tests/PHPStan/Analyser/data/bug-4711.php deleted file mode 100644 index f074b57db1..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4711.php +++ /dev/null @@ -1,19 +0,0 @@ -&nonEmpty', explode($string, '')); - assertType('array&nonEmpty', explode($string[0], '')); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-4715.php b/tests/PHPStan/Analyser/data/bug-4715.php index 14ea1ff1f2..508320fb8b 100644 --- a/tests/PHPStan/Analyser/data/bug-4715.php +++ b/tests/PHPStan/Analyser/data/bug-4715.php @@ -19,7 +19,7 @@ class ArrayCollection implements Collection /** * {@inheritDoc} */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator([]); } @@ -30,7 +30,7 @@ class Administration {} class Company { /** - * @var Collection|Administration[] + * @var Collection */ protected Collection $administrations; @@ -40,7 +40,7 @@ public function __construct() } /** - * @return Collection|Administration[] + * @return Collection */ public function getAdministrations() : Collection { diff --git a/tests/PHPStan/Analyser/data/bug-4732.php b/tests/PHPStan/Analyser/data/bug-4732.php new file mode 100644 index 0000000000..46f401919e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4732.php @@ -0,0 +1,23 @@ + $flags bitflags options + */ + public static function sayHello(int $flags): void + { + } + + public static function test(): void + { + HelloWorld::sayHello(HelloWorld::FOO_BAR | HelloWorld::FOO_BAZ); + HelloWorld::sayHello(HelloWorld::FOO_BAR); + HelloWorld::sayHello(HelloWorld::FOO_BAZ); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4902.php b/tests/PHPStan/Analyser/data/bug-4902.php index e56cd001dd..fc84a47d33 100644 --- a/tests/PHPStan/Analyser/data/bug-4902.php +++ b/tests/PHPStan/Analyser/data/bug-4902.php @@ -1,4 +1,4 @@ -= 7.4 + ...$wrappers */ function unwrapAllAndWrapAgain(Wrapper ...$wrappers): void { - assertType('array', array_map(function (Wrapper $item) { + assertType('list', array_map(function (Wrapper $item) { return $this->unwrap($item); }, $wrappers)); - assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); + assertType('list', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); } } diff --git a/tests/PHPStan/Analyser/data/bug-5017.php b/tests/PHPStan/Analyser/data/bug-5017.php deleted file mode 100644 index 94ac0efcf1..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5017.php +++ /dev/null @@ -1,56 +0,0 @@ -&nonEmpty', $items); - $batch = array_splice($items, 0, 2); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch); - } - } - - /** - * @param int[] $items - */ - public function doBar($items) - { - while ($items) { - assertType('array&nonEmpty', $items); - $batch = array_splice($items, 0, 2); - assertType('array', $items); - assertType('array', $batch); - } - } - - public function doBar2() - { - $items = [0, 1, 2, 3, 4]; - assertType('array(0, 1, 2, 3, 4)', $items); - $batch = array_splice($items, 0, 2); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch); - } - - /** - * @param int[] $ints - * @param string[] $strings - */ - public function doBar3(array $ints, array $strings) - { - $removed = array_splice($ints, 0, 2, $strings); - assertType('array', $removed); - assertType('array', $ints); - assertType('array', $strings); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-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-5219.php b/tests/PHPStan/Analyser/data/bug-5219.php deleted file mode 100644 index ab75d306a5..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5219.php +++ /dev/null @@ -1,25 +0,0 @@ -&nonEmpty', [$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/data/bug-5312.php b/tests/PHPStan/Analyser/data/bug-5312.php new file mode 100644 index 0000000000..95428adc4b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5312.php @@ -0,0 +1,14 @@ + + */ +interface Updatable +{ + /** + * @param T $object + */ + public function update(Updatable $object): void; +} diff --git a/tests/PHPStan/Analyser/data/bug-5390.php b/tests/PHPStan/Analyser/data/bug-5390.php new file mode 100644 index 0000000000..57332bebe3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5390.php @@ -0,0 +1,19 @@ +b->someMethod(); + } +} +/** @mixin A */ +class B +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-5597.php b/tests/PHPStan/Analyser/data/bug-5597.php new file mode 100644 index 0000000000..19720c8a17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5597.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug5597; + +interface InterfaceA {} + +class ClassA implements InterfaceA {} + +class ClassB +{ + public function __construct( + private InterfaceA $parameterA, + ) { + } + + public function test() : InterfaceA + { + return $this->parameterA; + } +} + +$classA = new class() extends ClassA {}; +$thisWorks = new class($classA) extends ClassB {}; + +$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {}; diff --git a/tests/PHPStan/Analyser/data/bug-5951.php b/tests/PHPStan/Analyser/data/bug-5951.php new file mode 100644 index 0000000000..a774868adc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5951.php @@ -0,0 +1,31 @@ += 8.0 + +namespace Bug5951; + +#[\Attribute] +class Route +{ + + /** @param string[] $methods */ + public function __construct(public string $path, public string $name, public array $methods) + { + + } + +} + +class Response +{ + +} + +final class SomeController +{ + public const ROUTE_INDEX = 'some_index'; + + #[Route('/some', name: self::ROUTE_INDEX, methods: ['GET'])] + public function index(): Response + { + return new Response(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-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..71723a5afe --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6300.php @@ -0,0 +1,30 @@ +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-to-numeric-string.php b/tests/PHPStan/Analyser/data/cast-to-numeric-string.php deleted file mode 100644 index e9e2259cb7..0000000000 --- a/tests/PHPStan/Analyser/data/cast-to-numeric-string.php +++ /dev/null @@ -1,61 +0,0 @@ -', $std::class); - assertType('*ERROR*', $string::class); - assertType('class-string', $stdOrNull::class); - assertType('*ERROR*', $stringOrNull::class); - } - -} diff --git a/tests/PHPStan/Analyser/data/closure-types.php b/tests/PHPStan/Analyser/data/closure-types.php deleted file mode 100644 index a0f60a0680..0000000000 --- a/tests/PHPStan/Analyser/data/closure-types.php +++ /dev/null @@ -1,50 +0,0 @@ - */ - private $arrayShapes; - - public function doFoo(): void - { - $a = array_map(function (array $a): array { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); - - return $a; - }, $this->arrayShapes); - assertType('array string, \'bar\' => int)>', $a); - - $b = array_map(function ($b) { - assertType('array(\'foo\' => string, \'bar\' => int)', $b); - - return $b['foo']; - }, $this->arrayShapes); - assertType('array', $b); - } - - public function doBar(): void - { - usort($this->arrayShapes, function (array $a, array $b): int { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); - assertType('array(\'foo\' => string, \'bar\' => int)', $b); - - return 1; - }); - } - - public function doBaz(): void - { - usort($this->arrayShapes, function ($a, $b): int { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); - assertType('array(\'foo\' => string, \'bar\' => int)', $b); - - return 1; - }); - } - -} diff --git a/tests/PHPStan/Analyser/data/coalesce-assign.php b/tests/PHPStan/Analyser/data/coalesce-assign.php index fd700ec914..e6940f5fbf 100644 --- a/tests/PHPStan/Analyser/data/coalesce-assign.php +++ b/tests/PHPStan/Analyser/data/coalesce-assign.php @@ -1,4 +1,4 @@ -= 7.4 + mixed)', compact(['foo' => '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/data/conditional-expression-infinite-loop.php b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php new file mode 100644 index 0000000000..9dda0d65b1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php @@ -0,0 +1,14 @@ + $foo = 1, + $isFoo || $isBar => $foo = 2, + default => $foo = null, + }; + } +} diff --git a/tests/PHPStan/Analyser/data/conditional-return-type-stub.php b/tests/PHPStan/Analyser/data/conditional-return-type-stub.php new file mode 100644 index 0000000000..0280837a8e --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-return-type-stub.php @@ -0,0 +1,36 @@ +doFoo(1)); + assertType('string', $f->doFoo("foo")); +}; + + +function (Bar $b): void { + assertType('int', $b->doFoo(1)); + assertType('string', $b->doFoo("foo")); +}; diff --git a/tests/PHPStan/Analyser/data/conditional-return-type.stub b/tests/PHPStan/Analyser/data/conditional-return-type.stub new file mode 100644 index 0000000000..41532de782 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-return-type.stub @@ -0,0 +1,16 @@ +', count($nonEmpty)); - assertType('int<1, max>', sizeof($nonEmpty)); - } - -} diff --git a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php index f79aea7dd3..420d5e089c 100644 --- a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php +++ b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php @@ -2,5 +2,5 @@ function bcompiler_write_file(): void { - + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/die-73.php b/tests/PHPStan/Analyser/data/die-73.php deleted file mode 100644 index b1bce4861f..0000000000 --- a/tests/PHPStan/Analyser/data/die-73.php +++ /dev/null @@ -1,3 +0,0 @@ - $array + * @param ( + * $mode is ARRAY_FILTER_USE_BOTH + * ? (callable(T, K=): bool) + * : ( + * $mode is ARRAY_FILTER_USE_KEY + * ? (callable(K): bool) + * : ( + * $mode is 0 + * ? (callable(T): bool) + * : null + * ) + * ) + * ) $callback + * @param ARRAY_FILTER_USE_BOTH|ARRAY_FILTER_USE_KEY|0 $mode + * + * @return array + */ +function filter(array $array, ?callable $callback = null, int $mode = ARRAY_FILTER_USE_BOTH): array +{ + return null !== $callback + ? array_filter($array, $callback, $mode) + : array_filter($array); +} + +function () { + // This one does fail, as both the value + key is asked and the key + value is used + filter( + [false, true, false], + static fn (int $key, bool $value): bool => 0 === $key % 2 && $value, + mode: ARRAY_FILTER_USE_BOTH, + ); + + // This one should fail, as both the value + key is asked but only the key is used + filter( + [false, true, false], + static fn (int $key): bool => 0 === $key % 2, + mode: ARRAY_FILTER_USE_BOTH, + ); + + // This one should fail, as only the key is asked but the value is used + filter( + [false, true, false], + static fn (bool $value): bool => $value, + mode: ARRAY_FILTER_USE_KEY, + ); + + // This one should fail, as only the value is asked but the key is used + filter( + [false, true, false], + static fn (int $key): bool => 0 === $key % 2, + mode: 0, + ); +}; diff --git a/tests/PHPStan/Analyser/data/discussion-9053.php b/tests/PHPStan/Analyser/data/discussion-9053.php new file mode 100644 index 0000000000..e02627602c --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9053.php @@ -0,0 +1,110 @@ += 8.0 + +namespace Discussion9053; + +use function PHPStan\Testing\assertType; + +/** + * @template TChild of ChildInterface + */ +interface ModelInterface { + /** + * @return TChild[] + */ + public function getChildren(): array; +} + +/** + * @implements ModelInterface + */ +class Model implements ModelInterface +{ + /** + * @var Child[] + */ + public array $children; + + public function getChildren(): array + { + return $this->children; + } +} + +/** + * @template T of ModelInterface + */ +interface ChildInterface { + /** + * @return T + */ + public function getModel(): ModelInterface; +} + + +/** + * @implements ChildInterface + */ +class Child implements ChildInterface +{ + public function __construct(private Model $model) + { + } + + public function getModel(): Model + { + return $this->model; + } +} + +/** + * @template T of ModelInterface + */ +class Helper +{ + /** + * @param T $model + */ + public function __construct(private ModelInterface $model) + {} + + /** + * @return template-type + */ + public function getFirstChildren(): ChildInterface + { + $firstChildren = $this->model->getChildren()[0] ?? null; + + if (!$firstChildren) { + throw new \RuntimeException('No first child found.'); + } + + return $firstChildren; + } +} + +class Other { + /** + * @template TChild of ChildInterface + * @template TModel of ModelInterface + * @param Helper $helper + * @return TChild + */ + public function getFirstChildren(Helper $helper): ChildInterface { + $child = $helper->getFirstChildren(); + assertType('TChild of Discussion9053\ChildInterface (method Discussion9053\Other::getFirstChildren(), argument)', $child); + + return $child; + } +} + +function (): void { + $model = new Model(); + $helper = new Helper($model); + assertType('Discussion9053\Helper', $helper); + $child = $helper->getFirstChildren(); + assertType('Discussion9053\Child', $child); + + $other = new Other(); + $child2 = $other->getFirstChildren($helper); + assertType('Discussion9053\Child', $child2); +}; diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/data/div-by-zero.php deleted file mode 100644 index 0c38a50c10..0000000000 --- a/tests/PHPStan/Analyser/data/div-by-zero.php +++ /dev/null @@ -1,23 +0,0 @@ - $range1 - * @param int $range2 - */ - public function doFoo(int $range1, int $range2, int $int): void - { - assertType('(float|int)', 5 / $range1); - assertType('(float|int)', 5 / $range2); - assertType('(float|int)', $range1 / $range2); - assertType('(float|int)', 5 / $int); - assertType('*ERROR*', 5 / 0); - } - -} diff --git a/tests/PHPStan/Analyser/data/do-not-pollute-scope-with-block.php b/tests/PHPStan/Analyser/data/do-not-pollute-scope-with-block.php new file mode 100644 index 0000000000..d1569b1d5b --- /dev/null +++ b/tests/PHPStan/Analyser/data/do-not-pollute-scope-with-block.php @@ -0,0 +1,26 @@ +', rand(0, 1)); - } - }; - - function (): void { - if (rand(0, 1) === 0) { - assertType('int<0, 1>', rand(0, 1)); - } - }; - function (): void { - assertType('1|\'foo\'', rand(0, 1) ?: 'foo'); - assertType('\'foo\'|int<0, 1>', rand(0, 1) ? rand(0, 1) : 'foo'); - }; - } - - public function doBar(): bool - { - - } - - /** @phpstan-pure */ - public function doBaz(): bool - { - - } - - /** @phpstan-impure */ - public function doLorem(): bool - { - - } - - public function doIpsum() - { - if ($this->doBar() === true) { - assertType('true', $this->doBar()); - } - - if ($this->doBaz() === true) { - assertType('true', $this->doBaz()); - } - - if ($this->doLorem() === true) { - assertType('bool', $this->doLorem()); - } - } - - public function doDolor() - { - if ($this->doBar()) { - assertType('true', $this->doBar()); - } - - if ($this->doBaz()) { - assertType('true', $this->doBaz()); - } - - if ($this->doLorem()) { - assertType('bool', $this->doLorem()); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php new file mode 100644 index 0000000000..5adbbc5220 --- /dev/null +++ b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php @@ -0,0 +1,111 @@ +pure() === 1) { + assertType('1', $this->pure()); + } + + if ($this->maybePure() === 1) { + assertType('int', $this->maybePure()); + } + + if ($this->impure() === 1) { + assertType('int', $this->impure()); + } + } + +} + +class FooStatic +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 1; + } + + public static function maybePure(): int + { + return 1; + } + + /** @phpstan-impure */ + public static function impure(): int + { + return rand(0, 1); + } + + public function test(): void + { + if (self::pure() === 1) { + assertType('1', self::pure()); + } + + if (self::maybePure() === 1) { + assertType('int', self::maybePure()); + } + + if (self::impure() === 1) { + assertType('int', self::impure()); + } + } + +} + +/** @phpstan-pure */ +function pure(): int +{ + return 1; +} + +function maybePure(): int +{ + return 1; +} + +/** @phpstan-impure */ +function impure(): int +{ + return rand(0, 1); +} + +function test(): void +{ + if (pure() === 1) { + assertType('1', pure()); + } + + if (maybePure() === 1) { + assertType('int', maybePure()); + } + + if (impure() === 1) { + assertType('int', impure()); + } +} diff --git a/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php new file mode 100644 index 0000000000..c39dca6e35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php @@ -0,0 +1,15 @@ += 8.3 + +namespace DynamicConstantNativeTypes; + +final class Foo +{ + + public const int FOO = 123; + public const int|string BAR = 123; + +} + +function (Foo $foo): void { + die; +}; diff --git a/tests/PHPStan/Analyser/data/dynamic-constant.php b/tests/PHPStan/Analyser/data/dynamic-constant.php index 2219d7698c..30bcf927bd 100644 --- a/tests/PHPStan/Analyser/data/dynamic-constant.php +++ b/tests/PHPStan/Analyser/data/dynamic-constant.php @@ -4,10 +4,12 @@ define('GLOBAL_PURE_CONSTANT', 123); define('GLOBAL_DYNAMIC_CONSTANT', false); +define('GLOBAL_DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES', null); class DynamicConstantClass { const DYNAMIC_CONSTANT_IN_CLASS = 'abcdef'; + const DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES_IN_CLASS = 'xyz'; const PURE_CONSTANT_IN_CLASS = 'abc123def'; } 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 56be77284a..0fb7c70d6d 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-return-types.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-types.php @@ -27,6 +27,7 @@ class InheritedEntityManager extends EntityManager class ComponentContainer implements \ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { @@ -37,11 +38,13 @@ public function offsetGet($offset): Entity } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php new file mode 100644 index 0000000000..7da45ec443 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php @@ -0,0 +1,68 @@ += 8.0 + +namespace DynamicMethodThrowTypeExtensionNamedArgs; + +use PHPStan\TrinaryLogic; +use function PHPStan\Testing\assertVariableCertainty; + +class Foo +{ + + /** @throws \Exception */ + public function throwOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + /** @throws \Exception */ + public static function staticThrowOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + public function doFoo1() + { + try { + $result = $this->throwOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo2() + { + try { + $result = $this->throwOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + + public function doFoo3() + { + try { + $result = self::staticThrowOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo4() + { + try { + $result = self::staticThrowOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} + diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php new file mode 100644 index 0000000000..727352a332 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php @@ -0,0 +1,60 @@ += 8.0 + +namespace DynamicMethodThrowTypeExtensionNamedArgs; + +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicMethodThrowTypeExtension; +use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; +use PHPStan\Type\Type; + +class MethodThrowTypeExtension implements DynamicMethodThrowTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'throwOrNot'; + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} + +class StaticMethodThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticThrowOrNot'; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-backed.php b/tests/PHPStan/Analyser/data/enum-reflection-backed.php new file mode 100644 index 0000000000..00f1b9634f --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-backed.php @@ -0,0 +1,16 @@ + $class */ +function testNarrowGetNameTypeAfterIsBacked(string $class) { + $r = new ReflectionEnum($class); + assertType('class-string', $r->getName()); + if ($r->isBacked()) { + assertType('class-string', $r->getName()); + } +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php81.php b/tests/PHPStan/Analyser/data/enum-reflection-php81.php new file mode 100644 index 0000000000..502de6eebd --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-php81.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumReflection81; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/data/enums-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/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php deleted file mode 100644 index bcdf815694..0000000000 --- a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php +++ /dev/null @@ -1,64 +0,0 @@ -doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } +} diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 173a4cd5f5..01b6d33f3d 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'); @@ -69,19 +56,6 @@ $gettimeofdayDefault = gettimeofday(null); $gettimeofdayBenevolent = gettimeofday($undefined); -// str_split -/** @var string $string */ -$string = doFoo(); -$strSplitConstantStringWithoutDefinedParameters = str_split(); -$strSplitConstantStringWithoutDefinedSplitLength = str_split('abcdef'); -$strSplitStringWithoutDefinedSplitLength = str_split($string); -$strSplitConstantStringWithOneSplitLength = str_split('abcdef', 1); -$strSplitConstantStringWithGreaterSplitLengthThanStringLength = str_split('abcdef', 999); -$strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); -$strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); -$strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); -$strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - // parse_url /** @var int $integer */ $integer = doFoo(); @@ -101,6 +75,8 @@ $stat = stat(__FILE__); $lstat = lstat(__FILE__); $fstat = fstat($resource); +$fileObject = new \SplFileObject(__FILE__); +$fileObjectStat = $fileObject->fstat(); $base64DecodeWithoutStrict = base64_decode(''); $base64DecodeWithStrictDisabled = base64_decode('', false); @@ -123,22 +99,4 @@ $integer = doFoo(); $strWordCountStrTypeIndeterminant = str_word_count('string', $integer); -$hashHmacMd5 = hash_hmac('md5', 'data', 'key'); -$hashHmacSha256 = hash_hmac('sha256', 'data', 'key'); -$hashHmacNonCryptographic = hash_hmac('crc32', 'data', 'key'); -$hashHmacRandom = hash_hmac('random', 'data', 'key'); -$hashHmacVariable = hash_hmac($string, 'data', 'key'); - -$hashHmacFileMd5 = hash_hmac_file('md5', 'data', 'key'); -$hashHmacFileSha256 = hash_hmac_file('sha256', 'data', 'key'); -$hashHmacFileNonCryptographic = hash_hmac_file('crc32', 'data', 'key'); -$hashHmacFileRandom = hash_hmac_file('random', 'data', 'key'); -$hashHmacFileVariable = hash_hmac_file($string, 'data', 'key'); - -$hash = hash('sha256', 'data', false); -$hashRaw = hash('sha256', 'data', true); -$hashRandom = hash('random', 'data', false); -/** @var mixed $mixed */ -$mixed = doFoo(); -$hashMixed = hash('md5', $mixed, false); die; diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/data/generic-class-string.php deleted file mode 100644 index 45f1dfaddf..0000000000 --- a/tests/PHPStan/Analyser/data/generic-class-string.php +++ /dev/null @@ -1,123 +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('object', new $a()); - - if (is_subclass_of($a, 'DateTimeInterface')) { - assertType('DateTimeInterface', $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()); - } - - if (is_subclass_of($a, C::class)) { - assertType('int', $a::f()); - } -} - -/** - * @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()); - } - - 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); - 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/data/generic-unions.php b/tests/PHPStan/Analyser/data/generic-unions.php deleted file mode 100644 index 7ebc5bb89b..0000000000 --- a/tests/PHPStan/Analyser/data/generic-unions.php +++ /dev/null @@ -1,63 +0,0 @@ -doFoo($nullableString)); - assertType('int|string', $this->doFoo($stringOrInt)); - - assertType('string|null', $this->doBar($nullableString)); - - assertType('int', $this->doBaz(1)); - assertType('string', $this->doBaz('foo')); - assertType('float', $this->doBaz(1.2)); - assertType('string', $this->doBaz($stringOrInt)); - } - -} diff --git a/tests/PHPStan/Analyser/data/getopt.php b/tests/PHPStan/Analyser/data/getopt.php deleted file mode 100644 index 142c986f57..0000000000 --- a/tests/PHPStan/Analyser/data/getopt.php +++ /dev/null @@ -1,9 +0,0 @@ -|string|false>|false)', $opts); diff --git a/tests/PHPStan/Analyser/data/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/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/impure-method.php b/tests/PHPStan/Analyser/data/impure-method.php deleted file mode 100644 index d471c0d54a..0000000000 --- a/tests/PHPStan/Analyser/data/impure-method.php +++ /dev/null @@ -1,81 +0,0 @@ -fooProp = rand(0, 1); - } - - public function ordinaryMethod(): int - { - return 1; - } - - /** - * @phpstan-impure - * @return int - */ - public function impureMethod(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - /** - * @impure - * @return int - */ - public function impureMethod2(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - public function doFoo(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->voidMethod(); - assertType('int', $this->fooProp); - } - - public function doBar(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->ordinaryMethod(); - assertType('1', $this->fooProp); - } - - public function doBaz(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod(); - assertType('int', $this->fooProp); - } - - public function doLorem(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod2(); - assertType('int', $this->fooProp); - } - -} diff --git a/tests/PHPStan/Analyser/data/in-array.php b/tests/PHPStan/Analyser/data/in-array.php deleted file mode 100644 index ca8246d0d1..0000000000 --- a/tests/PHPStan/Analyser/data/in-array.php +++ /dev/null @@ -1,47 +0,0 @@ -', $i); - - $i++; - assertType('int', $i); - } else { - assertType('int<3, max>', $i); - } - - if ($i < 3) { - assertType('int', $i); - - $i--; - assertType('int', $i); - } - - assertType('int|int<3, max>', $i); - - if ($i < 3 && $i > 5) { - assertType('*NEVER*', $i); - } else { - assertType('int|int<3, max>', $i); - } - - if ($i > 3 && $i < 5) { - assertType('4', $i); - } else { - assertType('3|int|int<5, max>', $i); - } - - if ($i >= 3 && $i <= 5) { - assertType('int<3, 5>', $i); - - if ($i === 2) { - assertType('*NEVER*', $i); - } else { - assertType('int<3, 5>', $i); - } - - if ($i !== 3) { - assertType('int<4, 5>', $i); - } else { - assertType('3', $i); - } - } -}; - - -function () { - for ($i = 0; $i < 5; $i++) { - assertType('int', $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<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 $rMin - * @param int<5, max> $rMax - * - * @param 20|40|60 $x - * @param 2|4 $y - */ - public function math($i, $j, $z, $pi, $r1, $r2, $rMin, $rMax, $x, $y) { - assertType('int', $r1 + $i); - assertType('int', $r1 - $i); - assertType('int', $r1 * $i); - assertType('(float|int)', $r1 / $i); - - assertType('int<2, 13>', $r1 + $j); - assertType('int<-2, 9>', $r1 - $j); - assertType('int<1, 30>', $r1 * $j); - assertType('float|int<0, 10>', $r1 / $j); - assertType('int', $rMin * $j); - assertType('int<5, max>', $rMax * $j); - - assertType('int<2, 13>', $j + $r1); - assertType('int<-9, 2>', $j - $r1); - assertType('int<1, 30>', $j * $r1); - assertType('float|int<0, 3>', $j / $r1); - assertType('int', $j * $rMin); - assertType('int<5, max>', $j * $rMax); - - assertType('int<-19, -10>|int<2, 13>', $r1 + $z); - assertType('int<-2, 9>|int<21, 30>', $r1 - $z); - assertType('int<-200, -20>|int<1, 30>', $r1 * $z); - assertType('float|int<0, 10>', $r1 / $z); - assertType('int', $rMin * $z); - assertType('int|int<5, max>', $rMax * $z); - - assertType('int<2, max>', $pi + 1); - assertType('int<-1, max>', $pi - 2); - assertType('int<2, max>', $pi * 2); - assertType('float|int<0, max>', $pi / 2); - assertType('int<2, max>', 1 + $pi); - assertType('int', 2 - $pi); - assertType('int<2, max>', 2 * $pi); - assertType('float|int<2, max>', 2 / $pi); - - assertType('int<5, 14>', $r1 + 4); - assertType('int<-3, 6>', $r1 - 4); - assertType('int<4, 40>', $r1 * 4); - assertType('float|int<0, 2>', $r1 / 4); - assertType('int<9, max>', $rMax + 4); - assertType('int<1, max>', $rMax - 4); - assertType('int<20, max>', $rMax * 4); - assertType('float|int<1, max>', $rMax / 4); - - assertType('int<6, 20>', $r1 + $r2); - assertType('int<-4, 0>', $r1 - $r2); - assertType('int<5, 100>', $r1 * $r2); - assertType('float|int<0, 1>', $r1 / $r2); - - assertType('int', $r1 + $rMin); - assertType('int', $r1 - $rMin); - assertType('int', $r1 * $rMin); - assertType('float|int', $r1 / $rMin); - assertType('int', $rMin + $r1); - assertType('int', $rMin - $r1); - assertType('int', $rMin * $r1); - assertType('float|int', $rMin / $r1); - - assertType('int<6, max>', $r1 + $rMax); - assertType('int', $r1 - $rMax); - assertType('int<5, max>', $r1 * $rMax); - assertType('float|int<0, max>', $r1 / $rMax); - assertType('int<6, max>', $rMax + $r1); - assertType('int<4, max>', $rMax - $r1); - assertType('int<5, max>', $rMax * $r1); - assertType('float|int<5, max>', $rMax / $r1); - - assertType('5|10|15|20|30', $x / $y); - - } - - /** - * @param int $rMin - * @param int<5, max> $rMax - * - * @see https://www.wolframalpha.com/input/?i=%28interval%5B2%2C%E2%88%9E%5D+%2F+-1%29 - * @see https://3v4l.org/ur9Wf - */ - public function maximaInversion($rMin, $rMax) { - assertType('int<-5, max>', -1 * $rMin); - assertType('int', -2 * $rMax); - - assertType('int<-5, max>', $rMin * -1); - assertType('int', $rMax * -2); - - assertType('float|int<0, max>', -1 / $rMin); - assertType('float|int', -2 / $rMax); - - assertType('float|int<-5, max>', $rMin / -1); - assertType('float|int', $rMax / -2); - } - - /** - * @param int<1, 10> $r1 - * @param int<-5, 10> $r2 - * @param int $rMin - * @param int<5, max> $rMax - * @param int<0, 50> $rZero - */ - public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { - - assertType('int<-10, -1>', -$r1); - assertType('int<-10, 5>', -$r2); - assertType('int<-5, max>', -$rMin); - assertType('int', -$rMax); - assertType('int<-50, 0>', -$rZero); - } - -} diff --git a/tests/PHPStan/Analyser/data/is-a.php b/tests/PHPStan/Analyser/data/is-a.php deleted file mode 100644 index 27bde886c0..0000000000 --- a/tests/PHPStan/Analyser/data/is-a.php +++ /dev/null @@ -1,28 +0,0 @@ - $fooClassString */ - $fooClassString = 'Foo'; - - if (is_a($foo, $fooClassString)) { - \PHPStan\Testing\assertType('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); - } -}; diff --git a/tests/PHPStan/Analyser/data/is-resource-specified.php b/tests/PHPStan/Analyser/data/is-resource-specified.php new file mode 100644 index 0000000000..c78d5ad75a --- /dev/null +++ b/tests/PHPStan/Analyser/data/is-resource-specified.php @@ -0,0 +1,11 @@ + $ints - */ - public function doFoo(Traversable $ints) - { - assertType('array', iterator_to_array($ints)); - } -} diff --git a/tests/PHPStan/Analyser/data/list-type.php b/tests/PHPStan/Analyser/data/list-type.php deleted file mode 100644 index 8fccd9f831..0000000000 --- a/tests/PHPStan/Analyser/data/list-type.php +++ /dev/null @@ -1,97 +0,0 @@ -', $list); - } - - /** @param list $list */ - public function directAssertionParamHint(array $list): void - { - assertType('array', $list); - } - - /** @param list $list */ - public function directAssertionNullableParamHint(array $list = null): void - { - assertType('array|null', $list); - } - - /** @param list<\DateTime> $list */ - public function directAssertionObjectParamHint($list): void - { - assertType('array', $list); - } - - public function withoutGenerics(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - $list[] = new \stdClass(); - assertType('array&nonEmpty', $list); - } - - - public function withMixedType(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - $list[] = new \stdClass(); - assertType('array&nonEmpty', $list); - } - - public function withObjectType(): void - { - /** @var list<\DateTime> $list */ - $list = []; - $list[] = new \DateTime(); - assertType('array&nonEmpty', $list); - } - - /** @return list */ - public function withScalarGoodContent(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - assertType('array&nonEmpty', $list); - } - - public function withNumericKey(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list['1'] = true; - assertType('array&nonEmpty', $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&nonEmpty', $list2); - } - -} diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/data/literal-string.php deleted file mode 100644 index aae44ef3f6..0000000000 --- a/tests/PHPStan/Analyser/data/literal-string.php +++ /dev/null @@ -1,52 +0,0 @@ -= 8.0 - -namespace MatchExpr; - -use function PHPStan\Testing\assertType; - -class Foo -{ - - /** - * @param 1|2|3|4 $i - */ - public function doFoo(int $i): void - { - assertType('*NEVER*', match ($i) { - 0 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1', match ($i) { - 1 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1|2', match ($i) { - 1, 2 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1|2|3', match ($i) { - 1, 2, 3 => $i, - }); - assertType('1|2|3|4', $i); - assertType('2|3', match ($i) { - 1 => exit(), - 2, 3 => $i, - }); - assertType('1|2|3|4', $i); - } - - /** - * @param 1|2|3|4 $i - */ - public function doBar(int $i): void - { - match ($i) { - 0 => assertType('*NEVER*', $i), - default => assertType('1|2|3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1 => assertType('1', $i), - default => assertType('2|3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1, 2 => assertType('1|2', $i), - default => assertType('3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1, 2, 3 => assertType('1|2|3', $i), - default => assertType('4', $i), - }; - assertType('1|2|3|4', $i); - - match ($i) { - assertType('1|2|3|4', $i), 1, assertType('2|3|4', $i) => null, - assertType('2|3|4', $i) => null, - default => assertType('2|3|4', $i), - }; - } - -} diff --git a/tests/PHPStan/Analyser/data/match-performance-issue.php b/tests/PHPStan/Analyser/data/match-performance-issue.php new file mode 100644 index 0000000000..b15e813423 --- /dev/null +++ b/tests/PHPStan/Analyser/data/match-performance-issue.php @@ -0,0 +1,1029 @@ += 8.1 + +namespace MatchPerformanceIssue; + +enum Country: string +{ + + case CODE_AFG = 'AF'; + case CODE_ALA = 'AX'; + case CODE_ALB = 'AL'; + case CODE_DZA = 'DZ'; + case CODE_ASM = 'AS'; + case CODE_AND = 'AD'; + case CODE_AGO = 'AO'; + case CODE_AIA = 'AI'; + case CODE_ATA = 'AQ'; + case CODE_ATG = 'AG'; + case CODE_ARG = 'AR'; + case CODE_ARM = 'AM'; + case CODE_ABW = 'AW'; + case CODE_AUS = 'AU'; + case CODE_AUT = 'AT'; + case CODE_AZE = 'AZ'; + case CODE_BHS = 'BS'; + case CODE_BHR = 'BH'; + case CODE_BGD = 'BD'; + case CODE_BRB = 'BB'; + case CODE_BLR = 'BY'; + case CODE_BEL = 'BE'; + case CODE_BLZ = 'BZ'; + case CODE_BEN = 'BJ'; + case CODE_BMU = 'BM'; + case CODE_BTN = 'BT'; + case CODE_BOL = 'BO'; + case CODE_BIH = 'BA'; + case CODE_BES = 'BQ'; + case CODE_BWA = 'BW'; + case CODE_BVT = 'BV'; + case CODE_BRA = 'BR'; + case CODE_IOT = 'IO'; + case CODE_BRN = 'BN'; + case CODE_BGR = 'BG'; + case CODE_BFA = 'BF'; + case CODE_BDI = 'BI'; + case CODE_KHM = 'KH'; + case CODE_CMR = 'CM'; + case CODE_CAN = 'CA'; + case CODE_CPV = 'CV'; + case CODE_CYM = 'KY'; + case CODE_CAF = 'CF'; + case CODE_TCD = 'TD'; + case CODE_CHL = 'CL'; + case CODE_CHN = 'CN'; + case CODE_CXR = 'CX'; + case CODE_CCK = 'CC'; + case CODE_COL = 'CO'; + case CODE_COM = 'KM'; + case CODE_COG = 'CG'; + case CODE_COK = 'CK'; + case CODE_CRI = 'CR'; + case CODE_CIV = 'CI'; + case CODE_HRV = 'HR'; + case CODE_CUB = 'CU'; + case CODE_CUW = 'CW'; + case CODE_CYP = 'CY'; + case CODE_CZE = 'CZ'; + case CODE_COD = 'CD'; + case CODE_DNK = 'DK'; + case CODE_DJI = 'DJ'; + case CODE_DMA = 'DM'; + case CODE_DOM = 'DO'; + case CODE_ECU = 'EC'; + case CODE_EGY = 'EG'; + case CODE_SLV = 'SV'; + case CODE_GNQ = 'GQ'; + case CODE_ERI = 'ER'; + case CODE_EST = 'EE'; + case CODE_ETH = 'ET'; + case CODE_FLK = 'FK'; + case CODE_FRO = 'FO'; + case CODE_FJI = 'FJ'; + case CODE_FIN = 'FI'; + case CODE_FRA = 'FR'; + case CODE_GUF = 'GF'; + case CODE_PYF = 'PF'; + case CODE_ATF = 'TF'; + case CODE_GAB = 'GA'; + case CODE_GMB = 'GM'; + case CODE_GEO = 'GE'; + case CODE_DEU = 'DE'; + case CODE_GHA = 'GH'; + case CODE_GIB = 'GI'; + case CODE_GRC = 'GR'; + case CODE_GRL = 'GL'; + case CODE_GRD = 'GD'; + case CODE_GLP = 'GP'; + case CODE_GUM = 'GU'; + case CODE_GTM = 'GT'; + case CODE_GGY = 'GG'; + case CODE_GIN = 'GN'; + case CODE_GNB = 'GW'; + case CODE_GUY = 'GY'; + case CODE_HTI = 'HT'; + case CODE_HMD = 'HM'; + case CODE_HND = 'HN'; + case CODE_HKG = 'HK'; + case CODE_HUN = 'HU'; + case CODE_ISL = 'IS'; + case CODE_IND = 'IN'; + case CODE_IDN = 'ID'; + case CODE_IRQ = 'IQ'; + case CODE_IRL = 'IE'; + case CODE_IRN = 'IR'; + case CODE_IMN = 'IM'; + case CODE_ISR = 'IL'; + case CODE_ITA = 'IT'; + case CODE_JAM = 'JM'; + case CODE_JPN = 'JP'; + case CODE_JEY = 'JE'; + case CODE_JOR = 'JO'; + case CODE_KAZ = 'KZ'; + case CODE_KEN = 'KE'; + case CODE_KIR = 'KI'; + case CODE_XKX = 'XK'; + case CODE_KWT = 'KW'; + case CODE_KGZ = 'KG'; + case CODE_LAO = 'LA'; + case CODE_LVA = 'LV'; + case CODE_LBN = 'LB'; + case CODE_LSO = 'LS'; + case CODE_LBR = 'LR'; + case CODE_LBY = 'LY'; + case CODE_LIE = 'LI'; + case CODE_LTU = 'LT'; + case CODE_LUX = 'LU'; + case CODE_MAC = 'MO'; + case CODE_MKD = 'MK'; + case CODE_MDG = 'MG'; + case CODE_MWI = 'MW'; + case CODE_MYS = 'MY'; + case CODE_MDV = 'MV'; + case CODE_MLI = 'ML'; + case CODE_MLT = 'MT'; + case CODE_MHL = 'MH'; + case CODE_MTQ = 'MQ'; + case CODE_MRT = 'MR'; + case CODE_MUS = 'MU'; + case CODE_MYT = 'YT'; + case CODE_MEX = 'MX'; + case CODE_FSM = 'FM'; + case CODE_MDA = 'MD'; + case CODE_MCO = 'MC'; + case CODE_MNG = 'MN'; + case CODE_MNE = 'ME'; + case CODE_MSR = 'MS'; + case CODE_MAR = 'MA'; + case CODE_MOZ = 'MZ'; + case CODE_MMR = 'MM'; + case CODE_NAM = 'NA'; + case CODE_NRU = 'NR'; + case CODE_NPL = 'NP'; + case CODE_NLD = 'NL'; + case CODE_NCL = 'NC'; + case CODE_NZL = 'NZ'; + case CODE_NIC = 'NI'; + case CODE_NER = 'NE'; + case CODE_NGA = 'NG'; + case CODE_NIU = 'NU'; + case CODE_NFK = 'NF'; + case CODE_MNP = 'MP'; + case CODE_PRK = 'KP'; + case CODE_NOR = 'NO'; + case CODE_OMN = 'OM'; + case CODE_PAK = 'PK'; + case CODE_PLW = 'PW'; + case CODE_PSE = 'PS'; + case CODE_PAN = 'PA'; + case CODE_PNG = 'PG'; + case CODE_PRY = 'PY'; + case CODE_PER = 'PE'; + case CODE_PHL = 'PH'; + case CODE_PCN = 'PN'; + case CODE_POL = 'PL'; + case CODE_PRT = 'PT'; + case CODE_PRI = 'PR'; + case CODE_QAT = 'QA'; + case CODE_REU = 'RE'; + case CODE_ROU = 'RO'; + case CODE_RUS = 'RU'; + case CODE_RWA = 'RW'; + case CODE_SHN = 'SH'; + case CODE_KNA = 'KN'; + case CODE_LCA = 'LC'; + case CODE_SXM = 'SX'; + case CODE_MAF = 'MF'; + case CODE_SPM = 'PM'; + case CODE_VCT = 'VC'; + case CODE_WSM = 'WS'; + case CODE_SMR = 'SM'; + case CODE_STP = 'ST'; + case CODE_SAU = 'SA'; + case CODE_SEN = 'SN'; + case CODE_SRB = 'RS'; + case CODE_SYC = 'SC'; + case CODE_SLE = 'SL'; + case CODE_SGP = 'SG'; + case CODE_SVK = 'SK'; + case CODE_SVN = 'SI'; + case CODE_SLB = 'SB'; + case CODE_SOM = 'SO'; + case CODE_ZAF = 'ZA'; + case CODE_SGS = 'GS'; + case CODE_KOR = 'KR'; + case CODE_SSU = 'SS'; + case CODE_ESP = 'ES'; + case CODE_LKA = 'LK'; + case CODE_SDN = 'SD'; + case CODE_SUR = 'SR'; + case CODE_SJM = 'SJ'; + case CODE_SWZ = 'SZ'; + case CODE_SWE = 'SE'; + case CODE_CHE = 'CH'; + case CODE_SYR = 'SY'; + case CODE_TWN = 'TW'; + case CODE_TJK = 'TJ'; + case CODE_TZA = 'TZ'; + case CODE_THA = 'TH'; + case CODE_TLS = 'TL'; + case CODE_TGO = 'TG'; + case CODE_TKL = 'TK'; + case CODE_TON = 'TO'; + case CODE_TTO = 'TT'; + case CODE_TUN = 'TN'; + case CODE_TUR = 'TR'; + case CODE_TKM = 'TM'; + case CODE_TCA = 'TC'; + case CODE_TUV = 'TV'; + case CODE_UGA = 'UG'; + case CODE_UKR = 'UA'; + case CODE_ARE = 'AE'; + case CODE_GBR = 'GB'; + case CODE_USA = 'US'; + case CODE_UMI = 'UM'; + case CODE_URY = 'UY'; + case CODE_UZB = 'UZ'; + case CODE_VUT = 'VU'; + case CODE_VAT = 'VA'; + case CODE_VEN = 'VE'; + case CODE_VNM = 'VN'; + case CODE_VGB = 'VG'; + case CODE_VIR = 'VI'; + case CODE_WLF = 'WF'; + case CODE_ESH = 'EH'; + case CODE_YEM = 'YE'; + case CODE_ZMB = 'ZM'; + case CODE_ZWE = 'ZW'; + + /** + * @return string[] + */ + public static function getLabelDefinitions(): array + { + return [ + self::CODE_ABW->value => _('Aruba'), + self::CODE_AFG->value => _('Islámský stát Afghánistán'), + self::CODE_AGO->value => _('Angolská republika'), + self::CODE_AIA->value => _('Anguilla'), + self::CODE_ALA->value => _('Alandské ostrovy'), + self::CODE_ALB->value => _('Albánská republika'), + self::CODE_AND->value => _('Andorrské knížectví'), + self::CODE_ARE->value => _('Spojené arabské emiráty'), + self::CODE_ARG->value => _('Argentinská republika'), + self::CODE_ARM->value => _('Arménská republika'), + self::CODE_ASM->value => _('Americká Samoa'), + self::CODE_ATA->value => _('Antarktida'), + self::CODE_ATF->value => _('Francouzská jižní území'), + self::CODE_ATG->value => _('Antigua a Barbuda'), + self::CODE_AUS->value => _('Austrálie'), + self::CODE_AUT->value => _('Rakouská republika'), + self::CODE_AZE->value => _('Ázerbájdžánská republika'), + self::CODE_BDI->value => _('Burundská republika'), + self::CODE_BEL->value => _('Belgické království'), + self::CODE_BEN->value => _('Beninská republika'), + self::CODE_BFA->value => _('Burkina Faso'), + self::CODE_BGD->value => _('Bangladéšská lidová republika'), + self::CODE_BGR->value => _('Bulharská republika'), + self::CODE_BHR->value => _('Bahrajnské království'), + self::CODE_BHS->value => _('Bahamské společenství'), + self::CODE_BIH->value => _('Bosna a Hercegovina'), + self::CODE_BES->value => _('Karibské Nizozemsko'), + self::CODE_BLR->value => _('Běloruská republika'), + self::CODE_BLZ->value => _('Belize'), + self::CODE_BMU->value => _('Bermudy'), + self::CODE_BOL->value => _('Bolivijská republika'), + self::CODE_BRA->value => _('Brazilská federativní republika'), + self::CODE_BRB->value => _('Barbados'), + self::CODE_BRN->value => _('Brunej Darussalam'), + self::CODE_BTN->value => _('Bhútánské království'), + self::CODE_BVT->value => _('Bouvetův ostrov'), + self::CODE_BWA->value => _('Botswanská republika'), + self::CODE_CAF->value => _('Středoafrická republika'), + self::CODE_CAN->value => _('Kanada'), + self::CODE_CCK->value => _('Kokosové ostrovy'), + self::CODE_CHE->value => _('Švýcarská konfederace'), + self::CODE_CHL->value => _('Chilská republika'), + self::CODE_CHN->value => _('Čínská lidová republika'), + self::CODE_CIV->value => _('Republika Pobřeží slonoviny'), + self::CODE_CMR->value => _('Kamerunská republika'), + self::CODE_COD->value => _('Konžská demokratická republika'), + self::CODE_COG->value => _('Konžská republika'), + self::CODE_COK->value => _('Cookovy ostrovy'), + self::CODE_COL->value => _('Kolumbijská republika'), + self::CODE_COM->value => _('Komorský svaz'), + self::CODE_CPV->value => _('Kapverdská republika'), + self::CODE_CRI->value => _('Kostarická republika'), + self::CODE_CUB->value => _('Kubánská republika'), + self::CODE_CUW->value => _('Curaçao'), + self::CODE_CXR->value => _('Vánoční ostrov'), + self::CODE_CYM->value => _('Kajmanské ostrovy'), + self::CODE_CYP->value => _('Kyperská republika'), + self::CODE_CZE->value => _('Česká republika'), + self::CODE_DEU->value => _('Spolková republika Německo'), + self::CODE_DJI->value => _('Džibutská republika'), + self::CODE_DMA->value => _('Dominické společenství'), + self::CODE_DNK->value => _('Dánské království'), + self::CODE_DOM->value => _('Dominikánská republika'), + self::CODE_DZA->value => _('Alžírská lidová demokratická republika'), + self::CODE_ECU->value => _('Ekvádorská republika'), + self::CODE_EGY->value => _('Egyptská arabská republika'), + self::CODE_ERI->value => _('Eritrea'), + self::CODE_ESH->value => _('Západní Sahara'), + self::CODE_ESP->value => _('Španělské království'), + self::CODE_EST->value => _('Estonská republika'), + self::CODE_ETH->value => _('Etiopská federativní demokratická republika'), + self::CODE_FIN->value => _('Finská republika'), + self::CODE_FJI->value => _('Republika Fidžijské ostrovy'), + self::CODE_FLK->value => _('Falklandy (Malvíny)'), + self::CODE_FRA->value => _('Francouzská republika'), + self::CODE_FRO->value => _('Faerské ostrovy'), + self::CODE_FSM->value => _('Federativní státy Mikronésie'), + self::CODE_GAB->value => _('Gabonská republika'), + self::CODE_GBR->value => _('Spojené království Velké Británie a Severního Irska'), + self::CODE_GEO->value => _('Gruzie'), + self::CODE_GGY->value => _('Guernsey'), + self::CODE_GHA->value => _('Ghanská republika'), + self::CODE_GIB->value => _('Gibraltar'), + self::CODE_GIN->value => _('Guinejská republika'), + self::CODE_GLP->value => _('Guadeloupe'), + self::CODE_GMB->value => _('Gambijská republika'), + self::CODE_GNB->value => _('Republika Guinea-Bissau'), + self::CODE_GNQ->value => _('Republika Rovníková Guinea'), + self::CODE_GRC->value => _('Řecká republika'), + self::CODE_GRD->value => _('Grenada'), + self::CODE_GRL->value => _('Grónsko'), + self::CODE_GTM->value => _('Guatemalská republika'), + self::CODE_GUF->value => _('Francouzská Guyana'), + self::CODE_GUM->value => _('Guam'), + self::CODE_GUY->value => _('Guyanská republika'), + self::CODE_HKG->value => _('Hongkong, zvláštní administrativní oblast Čínské lidové republiky'), + self::CODE_HMD->value => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND->value => _('Honduraská republika'), + self::CODE_HRV->value => _('Chorvatská republika'), + self::CODE_HTI->value => _('Haitská republika'), + self::CODE_HUN->value => _('Maďarská republika'), + self::CODE_IDN->value => _('Indonéská republika'), + self::CODE_IMN->value => _('Ostrov Man'), + self::CODE_IND->value => _('Indická republika'), + self::CODE_IOT->value => _('Britské indickooceánské území'), + self::CODE_IRL->value => _('Irsko'), + self::CODE_IRN->value => _('Íránská islámská republika'), + self::CODE_IRQ->value => _('Irácká republika'), + self::CODE_ISL->value => _('Islandská republika'), + self::CODE_ISR->value => _('Izraelský stát'), + self::CODE_ITA->value => _('Italská republika'), + self::CODE_JAM->value => _('Jamajka'), + self::CODE_JEY->value => _('Jersey'), + self::CODE_JOR->value => _('Jordánské hášimovské království'), + self::CODE_JPN->value => _('Japonsko'), + self::CODE_KAZ->value => _('Republika Kazachstán'), + self::CODE_KEN->value => _('Keňská republika'), + self::CODE_KGZ->value => _('Republika Kyrgyzstán'), + self::CODE_KHM->value => _('Kambodžské království'), + self::CODE_KIR->value => _('Republika Kiribati'), + self::CODE_KNA->value => _('Svatý Kryštof a Nevis'), + self::CODE_KOR->value => _('Korejská republika'), + self::CODE_KWT->value => _('Kuvajtský stát'), + self::CODE_LAO->value => _('Laoská lidově demokratická republika'), + self::CODE_LBN->value => _('Libanonská republika'), + self::CODE_LBR->value => _('Liberijská republika'), + self::CODE_LBY->value => _('Libyjská arabská lidová socialistická džamáhírije'), + self::CODE_LCA->value => _('Svatá Lucie'), + self::CODE_LIE->value => _('Lichtenštejnské knížectví'), + self::CODE_LKA->value => _('Srílanská demokratická socialistická republika'), + self::CODE_LSO->value => _('Lesothské království'), + self::CODE_LTU->value => _('Litevská republika'), + self::CODE_LUX->value => _('Lucemburské velkovévodství'), + self::CODE_LVA->value => _('Lotyšská republika'), + self::CODE_MAC->value => _('Macao, zvláštní administrativní oblast Čínské lidové republiky'), + self::CODE_MAF->value => _('Svatý Martin (Francie)'), + self::CODE_MAR->value => _('Marocké království'), + self::CODE_MCO->value => _('Monacké knížectví'), + self::CODE_MDA->value => _('Moldavská republika'), + self::CODE_MDG->value => _('Madagaskarská republika'), + self::CODE_MDV->value => _('Maledivská republika'), + self::CODE_MEX->value => _('Spojené státy mexické'), + self::CODE_MHL->value => _('Republika Marshallovy ostrovy'), + self::CODE_MKD->value => _('Bývalá jugoslávská republika Makedonie'), + self::CODE_MLI->value => _('Maliská republika'), + self::CODE_MLT->value => _('Maltská republika'), + self::CODE_MMR->value => _('Myanmarský svaz'), + self::CODE_MNE->value => _('Republika Černá Hora'), + self::CODE_MNG->value => _('Mongolsko'), + self::CODE_MNP->value => _('Společenství Severních Marian'), + self::CODE_MOZ->value => _('Mosambická republika'), + self::CODE_MRT->value => _('Mauritánská islámská republika'), + self::CODE_MSR->value => _('Montserrat'), + self::CODE_MTQ->value => _('Martinik'), + self::CODE_MUS->value => _('Mauricijská republika'), + self::CODE_MWI->value => _('Malawská republika'), + self::CODE_MYS->value => _('Malajsie'), + self::CODE_MYT->value => _('Mayotte'), + self::CODE_NAM->value => _('Namibijská republika'), + self::CODE_NCL->value => _('Nová Kaledonie'), + self::CODE_NER->value => _('Nigerská republika'), + self::CODE_NFK->value => _('Norfolk'), + self::CODE_NGA->value => _('Nigérijská federativní republika'), + self::CODE_NIC->value => _('Nikaragujská republika'), + self::CODE_NIU->value => _('Niue'), + self::CODE_NLD->value => _('Nizozemské království'), + self::CODE_NOR->value => _('Norské království'), + self::CODE_NPL->value => _('Nepálské království'), + self::CODE_NRU->value => _('Nauruská republika'), + self::CODE_NZL->value => _('Nový Zéland'), + self::CODE_OMN->value => _('Sultanát Omán'), + self::CODE_PAK->value => _('Pákistánská islámská republika'), + self::CODE_PAN->value => _('Panamská republika'), + self::CODE_PCN->value => _('Pitcairn'), + self::CODE_PER->value => _('Peruánská republika'), + self::CODE_PHL->value => _('Filipínská republika'), + self::CODE_PLW->value => _('Palauská republika'), + self::CODE_PNG->value => _('Papua Nová Guinea'), + self::CODE_POL->value => _('Polská republika'), + self::CODE_PRI->value => _('Portoriko'), + self::CODE_PRK->value => _('Korejská lidově demokratická republika'), + self::CODE_PRT->value => _('Portugalská republika'), + self::CODE_PRY->value => _('Paraguayská republika'), + self::CODE_PSE->value => _('Palestinská samospráva'), + self::CODE_PYF->value => _('Francouzská Polynésie'), + self::CODE_QAT->value => _('Stát Katar'), + self::CODE_REU->value => _('Réunion'), + self::CODE_ROU->value => _('Rumunsko'), + self::CODE_RUS->value => _('Ruská federace'), + self::CODE_RWA->value => _('Rwandská republika'), + self::CODE_SAU->value => _('Saúdskoarabské království'), + self::CODE_SDN->value => _('Súdánská republika'), + self::CODE_SEN->value => _('Senegalská republika'), + self::CODE_SGP->value => _('Singapurská republika'), + self::CODE_SGS->value => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN->value => _('Svatá Helena'), + self::CODE_SJM->value => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB->value => _('Šalamounovy ostrovy'), + self::CODE_SLE->value => _('Republika Sierra Leone'), + self::CODE_SLV->value => _('Salvadorská republika'), + self::CODE_SMR->value => _('Sanmarinská republika'), + self::CODE_SOM->value => _('Somálská republika'), + self::CODE_SPM->value => _('Saint Pierre a Miquelon'), + self::CODE_SRB->value => _('Republika Srbsko'), + self::CODE_SSU->value => _('Jihosúdánská republika'), + self::CODE_STP->value => _('Demokratická republika Svatý Tomáš a Princův ostrov'), + self::CODE_SUR->value => _('Surinamská republika'), + self::CODE_SVK->value => _('Slovenská republika'), + self::CODE_SVN->value => _('Slovinská republika'), + self::CODE_SWE->value => _('Švédské království'), + self::CODE_SWZ->value => _('Svazijské království'), + self::CODE_SXM->value => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC->value => _('Seychelská republika'), + self::CODE_SYR->value => _('Syrská arabská republika'), + self::CODE_TCA->value => _('Turks a Caicos'), + self::CODE_TCD->value => _('Čadská republika'), + self::CODE_TGO->value => _('Tožská republika'), + self::CODE_THA->value => _('Thajské království'), + self::CODE_TJK->value => _('Republika Tádžikistán'), + self::CODE_TKL->value => _('Tokelau'), + self::CODE_TKM->value => _('Turkmenistán'), + self::CODE_TLS->value => _('Demokratická republika Východní Timor'), + self::CODE_TON->value => _('Království Tonga'), + self::CODE_TTO->value => _('Republika Trinidad a Tobago'), + self::CODE_TUN->value => _('Tuniská republika'), + self::CODE_TUR->value => _('Turecká republika'), + self::CODE_TUV->value => _('Tuvalu'), + self::CODE_TWN->value => _('Tchaj-wan, čínská provincie'), + self::CODE_TZA->value => _('Sjednocená republika Tanzanie'), + self::CODE_UGA->value => _('Ugandská republika'), + self::CODE_UKR->value => _('Ukrajina'), + self::CODE_UMI->value => _('Menší odlehlé ostrovy USA'), + self::CODE_URY->value => _('Uruguayská východní republika'), + self::CODE_USA->value => _('Spojené státy americké'), + self::CODE_UZB->value => _('Republika Uzbekistán'), + self::CODE_VAT->value => _('Svatý stolec (Vatikánský městský stát)'), + self::CODE_VCT->value => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN->value => _('Bolívarovská republika Venezuela'), + self::CODE_VGB->value => _('Britské Panenské ostrovy'), + self::CODE_VIR->value => _('Americké Panenské ostrovy'), + self::CODE_VNM->value => _('Vietnamská socialistická republika'), + self::CODE_VUT->value => _('Vanuatská republika'), + self::CODE_WLF->value => _('Wallis a Futuna'), + self::CODE_WSM->value => _('Nezávislý stát Samoa'), + self::CODE_XKX->value => _('Kosovská republika'), + self::CODE_YEM->value => _('Jemenská republika'), + self::CODE_ZAF->value => _('Jihoafrická republika'), + self::CODE_ZMB->value => _('Zambijská republika'), + self::CODE_ZWE->value => _('Zimbabwská republika'), + ]; + } + + /** + * @return string[] + */ + public static function getShortLabelDefinitions(): array + { + return [ + self::CODE_ABW->value => _('Aruba'), + self::CODE_AFG->value => _('Afghánistán'), + self::CODE_AGO->value => _('Angola'), + self::CODE_AIA->value => _('Anguilla'), + self::CODE_ALA->value => _('Alandské ostrovy'), + self::CODE_ALB->value => _('Albánie'), + self::CODE_AND->value => _('Andorra'), + self::CODE_ARE->value => _('Spojené arabské emiráty'), + self::CODE_ARG->value => _('Argentina'), + self::CODE_ARM->value => _('Arménie'), + self::CODE_ASM->value => _('Americká Samoa'), + self::CODE_ATA->value => _('Antarktida'), + self::CODE_ATF->value => _('Francouzská jižní území'), + self::CODE_ATG->value => _('Antigua a Barbuda'), + self::CODE_AUS->value => _('Austrálie'), + self::CODE_AUT->value => _('Rakousko'), + self::CODE_AZE->value => _('Ázerbájdžán'), + self::CODE_BDI->value => _('Burundi'), + self::CODE_BEL->value => _('Belgie'), + self::CODE_BEN->value => _('Benin'), + self::CODE_BFA->value => _('Burkina Faso'), + self::CODE_BGD->value => _('Bangladéš'), + self::CODE_BGR->value => _('Bulharsko'), + self::CODE_BHR->value => _('Bahrajn'), + self::CODE_BHS->value => _('Bahamy'), + self::CODE_BIH->value => _('Bosna a Hercegovina'), + self::CODE_BES->value => _('Karibské Nizozemsko'), + self::CODE_BLR->value => _('Bělorusko'), + self::CODE_BLZ->value => _('Belize'), + self::CODE_BMU->value => _('Bermudy'), + self::CODE_BOL->value => _('Bolívie'), + self::CODE_BRA->value => _('Brazílie'), + self::CODE_BRB->value => _('Barbados'), + self::CODE_BRN->value => _('Brunej Darussalam'), + self::CODE_BTN->value => _('Bhútán'), + self::CODE_BVT->value => _('Bouvetův ostrov'), + self::CODE_BWA->value => _('Botswana'), + self::CODE_CAF->value => _('Středoafrická republika'), + self::CODE_CAN->value => _('Kanada'), + self::CODE_CCK->value => _('Kokosové ostrovy'), + self::CODE_CHE->value => _('Švýcarsko'), + self::CODE_CHL->value => _('Chile'), + self::CODE_CHN->value => _('Čína'), + self::CODE_CIV->value => _('Pobřeží slonoviny'), + self::CODE_CMR->value => _('Kamerun'), + self::CODE_COD->value => _('Kongo, demokratická republika'), + self::CODE_COG->value => _('Kongo'), + self::CODE_COK->value => _('Cookovy ostrovy'), + self::CODE_COL->value => _('Kolumbie'), + self::CODE_COM->value => _('Komory'), + self::CODE_CPV->value => _('Kapverdy'), + self::CODE_CRI->value => _('Kostarika'), + self::CODE_CUB->value => _('Kuba'), + self::CODE_CUW->value => _('Curaçao'), + self::CODE_CXR->value => _('Vánoční ostrov'), + self::CODE_CYM->value => _('Kajmanské ostrovy'), + self::CODE_CYP->value => _('Kypr'), + self::CODE_CZE->value => _('Česko'), + self::CODE_DEU->value => _('Německo'), + self::CODE_DJI->value => _('Džibutsko'), + self::CODE_DMA->value => _('Dominika'), + self::CODE_DNK->value => _('Dánsko'), + self::CODE_DOM->value => _('Dominikánská republika'), + self::CODE_DZA->value => _('Alžírsko'), + self::CODE_ECU->value => _('Ekvádor'), + self::CODE_EGY->value => _('Egypt'), + self::CODE_ERI->value => _('Eritrea'), + self::CODE_ESH->value => _('Západní Sahara'), + self::CODE_ESP->value => _('Španělsko'), + self::CODE_EST->value => _('Estonsko'), + self::CODE_ETH->value => _('Etiopie'), + self::CODE_FIN->value => _('Finsko'), + self::CODE_FJI->value => _('Fidži'), + self::CODE_FLK->value => _('Falklandy (Malvíny)'), + self::CODE_FRA->value => _('Francie'), + self::CODE_FRO->value => _('Faerské ostrovy'), + self::CODE_FSM->value => _('Mikronésie'), + self::CODE_GAB->value => _('Gabon'), + self::CODE_GBR->value => _('Velká Británie'), + self::CODE_GEO->value => _('Gruzie'), + self::CODE_GGY->value => _('Guernsey'), + self::CODE_GHA->value => _('Ghana'), + self::CODE_GIB->value => _('Gibraltar'), + self::CODE_GIN->value => _('Guinea'), + self::CODE_GLP->value => _('Guadeloupe'), + self::CODE_GMB->value => _('Gambie'), + self::CODE_GNB->value => _('Guinea-Bissau'), + self::CODE_GNQ->value => _('Rovníková Guinea'), + self::CODE_GRC->value => _('Řecko'), + self::CODE_GRD->value => _('Grenada'), + self::CODE_GRL->value => _('Grónsko'), + self::CODE_GTM->value => _('Guatemala'), + self::CODE_GUF->value => _('Francouzská Guyana'), + self::CODE_GUM->value => _('Guam'), + self::CODE_GUY->value => _('Guyana'), + self::CODE_HKG->value => _('Hongkong'), + self::CODE_HMD->value => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND->value => _('Honduras'), + self::CODE_HRV->value => _('Chorvatsko'), + self::CODE_HTI->value => _('Haiti'), + self::CODE_HUN->value => _('Maďarsko'), + self::CODE_IDN->value => _('Indonésie'), + self::CODE_IMN->value => _('Ostrov Man'), + self::CODE_IND->value => _('Indie'), + self::CODE_IOT->value => _('Britské indickooceánské území'), + self::CODE_IRL->value => _('Irsko'), + self::CODE_IRN->value => _('Írán'), + self::CODE_IRQ->value => _('Irák'), + self::CODE_ISL->value => _('Island'), + self::CODE_ISR->value => _('Izrael'), + self::CODE_ITA->value => _('Itálie'), + self::CODE_JAM->value => _('Jamajka'), + self::CODE_JEY->value => _('Jersey'), + self::CODE_JOR->value => _('Jordánsko'), + self::CODE_JPN->value => _('Japonsko'), + self::CODE_KAZ->value => _('Kazachstán'), + self::CODE_KEN->value => _('Keňa'), + self::CODE_KGZ->value => _('Kyrgyzstán'), + self::CODE_KHM->value => _('Kambodža'), + self::CODE_KIR->value => _('Kiribati'), + self::CODE_KNA->value => _('Svatý Kryštof a Nevis'), + self::CODE_KOR->value => _('Jižní Korea'), + self::CODE_KWT->value => _('Kuvajt'), + self::CODE_LAO->value => _('Laos'), + self::CODE_LBN->value => _('Libanon'), + self::CODE_LBR->value => _('Libérie'), + self::CODE_LBY->value => _('Libye'), + self::CODE_LCA->value => _('Svatá Lucie'), + self::CODE_LIE->value => _('Lichtenštejnsko'), + self::CODE_LKA->value => _('Srí Lanka'), + self::CODE_LSO->value => _('Lesotho'), + self::CODE_LTU->value => _('Litva'), + self::CODE_LUX->value => _('Lucembursko'), + self::CODE_LVA->value => _('Lotyšsko'), + self::CODE_MAC->value => _('Macao'), + self::CODE_MAF->value => _('Svatý Martin (Francie)'), + self::CODE_MAR->value => _('Maroko'), + self::CODE_MCO->value => _('Monako'), + self::CODE_MDA->value => _('Moldavsko'), + self::CODE_MDG->value => _('Madagaskar'), + self::CODE_MDV->value => _('Maledivy'), + self::CODE_MEX->value => _('Mexiko'), + self::CODE_MHL->value => _('Marshallovy ostrovy'), + self::CODE_MKD->value => _('Makedonie'), + self::CODE_MLI->value => _('Mali'), + self::CODE_MLT->value => _('Malta'), + self::CODE_MMR->value => _('Myanmar'), + self::CODE_MNE->value => _('Černá Hora'), + self::CODE_MNG->value => _('Mongolsko'), + self::CODE_MNP->value => _('Severní Mariany'), + self::CODE_MOZ->value => _('Mosambik'), + self::CODE_MRT->value => _('Mauritánie'), + self::CODE_MSR->value => _('Montserrat'), + self::CODE_MTQ->value => _('Martinik'), + self::CODE_MUS->value => _('Mauricius'), + self::CODE_MWI->value => _('Malawi'), + self::CODE_MYS->value => _('Malajsie'), + self::CODE_MYT->value => _('Mayotte'), + self::CODE_NAM->value => _('Namibie'), + self::CODE_NCL->value => _('Nová Kaledonie'), + self::CODE_NER->value => _('Niger'), + self::CODE_NFK->value => _('Norfolk'), + self::CODE_NGA->value => _('Nigérie'), + self::CODE_NIC->value => _('Nikaragua'), + self::CODE_NIU->value => _('Niue'), + self::CODE_NLD->value => _('Nizozemsko'), + self::CODE_NOR->value => _('Norsko'), + self::CODE_NPL->value => _('Nepál'), + self::CODE_NRU->value => _('Nauru'), + self::CODE_NZL->value => _('Nový Zéland'), + self::CODE_OMN->value => _('Omán'), + self::CODE_PAK->value => _('Pákistán'), + self::CODE_PAN->value => _('Panama'), + self::CODE_PCN->value => _('Pitcairn'), + self::CODE_PER->value => _('Peru'), + self::CODE_PHL->value => _('Filipíny'), + self::CODE_PLW->value => _('Palau'), + self::CODE_PNG->value => _('Papua Nová Guinea'), + self::CODE_POL->value => _('Polsko'), + self::CODE_PRI->value => _('Portoriko'), + self::CODE_PRK->value => _('Severní Korea'), + self::CODE_PRT->value => _('Portugalsko'), + self::CODE_PRY->value => _('Paraguay'), + self::CODE_PSE->value => _('Palestina'), + self::CODE_PYF->value => _('Francouzská Polynésie'), + self::CODE_QAT->value => _('Katar'), + self::CODE_REU->value => _('Réunion'), + self::CODE_ROU->value => _('Rumunsko'), + self::CODE_RUS->value => _('Rusko'), + self::CODE_RWA->value => _('Rwanda'), + self::CODE_SAU->value => _('Saúdská Arábie'), + self::CODE_SDN->value => _('Súdán'), + self::CODE_SEN->value => _('Senegal'), + self::CODE_SGP->value => _('Singapur'), + self::CODE_SGS->value => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN->value => _('Svatá Helena'), + self::CODE_SJM->value => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB->value => _('Šalamounovy ostrovy'), + self::CODE_SLE->value => _('Sierra Leone'), + self::CODE_SLV->value => _('Salvador'), + self::CODE_SMR->value => _('San Marino'), + self::CODE_SOM->value => _('Somálsko'), + self::CODE_SPM->value => _('Saint Pierre a Miquelon'), + self::CODE_SRB->value => _('Srbsko'), + self::CODE_SSU->value => _('Jižní Súdán'), + self::CODE_STP->value => _('Svatý Tomáš'), + self::CODE_SUR->value => _('Surinam'), + self::CODE_SVK->value => _('Slovensko'), + self::CODE_SVN->value => _('Slovinsko'), + self::CODE_SWE->value => _('Švédsko'), + self::CODE_SWZ->value => _('Svazijsko'), + self::CODE_SXM->value => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC->value => _('Seychely'), + self::CODE_SYR->value => _('Sýrie'), + self::CODE_TCA->value => _('Turks a Caicos'), + self::CODE_TCD->value => _('Čad'), + self::CODE_TGO->value => _('Togo'), + self::CODE_THA->value => _('Thajsko'), + self::CODE_TJK->value => _('Tádžikistán'), + self::CODE_TKL->value => _('Tokelau'), + self::CODE_TKM->value => _('Turkmenistán'), + self::CODE_TLS->value => _('Východní Timor'), + self::CODE_TON->value => _('Tonga'), + self::CODE_TTO->value => _('Trinidad a Tobago'), + self::CODE_TUN->value => _('Tunisko'), + self::CODE_TUR->value => _('Turecko'), + self::CODE_TUV->value => _('Tuvalu'), + self::CODE_TWN->value => _('Tchaj-wan'), + self::CODE_TZA->value => _('Tanzanie'), + self::CODE_UGA->value => _('Uganda'), + self::CODE_UKR->value => _('Ukrajina'), + self::CODE_UMI->value => _('Menší odlehlé ostrovy USA'), + self::CODE_URY->value => _('Uruguay'), + self::CODE_USA->value => _('Spojené státy'), + self::CODE_UZB->value => _('Uzbekistán'), + self::CODE_VAT->value => _('Vatikán'), + self::CODE_VCT->value => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN->value => _('Venezuela'), + self::CODE_VGB->value => _('Britské Panenské ostrovy'), + self::CODE_VIR->value => _('Americké Panenské ostrovy'), + self::CODE_VNM->value => _('Vietnam'), + self::CODE_VUT->value => _('Vanuatu'), + self::CODE_WLF->value => _('Wallis a Futuna'), + self::CODE_WSM->value => _('Samoa'), + self::CODE_XKX->value => _('Kosovo'), + self::CODE_YEM->value => _('Jemen'), + self::CODE_ZAF->value => _('Jihoafrická republika'), + self::CODE_ZMB->value => _('Zambie'), + self::CODE_ZWE->value => _('Zimbabwe'), + ]; + } + + public function getIdent(): string + { + return match ($this) { + self::CODE_ABW => _('Aruba'), + self::CODE_AFG => _('Afghánistán'), + self::CODE_AGO => _('Angola'), + self::CODE_AIA => _('Anguilla'), + self::CODE_ALA => _('Alandské ostrovy'), + self::CODE_ALB => _('Albánie'), + self::CODE_AND => _('Andorra'), + self::CODE_ARE => _('Spojené arabské emiráty'), + self::CODE_ARG => _('Argentina'), + self::CODE_ARM => _('Arménie'), + self::CODE_ASM => _('Americká Samoa'), + self::CODE_ATA => _('Antarktida'), + self::CODE_ATF => _('Francouzská jižní území'), + self::CODE_ATG => _('Antigua a Barbuda'), + self::CODE_AUS => _('Austrálie'), + self::CODE_AUT => _('Rakousko'), + self::CODE_AZE => _('Ázerbájdžán'), + self::CODE_BDI => _('Burundi'), + self::CODE_BEL => _('Belgie'), + self::CODE_BEN => _('Benin'), + self::CODE_BFA => _('Burkina Faso'), + self::CODE_BGD => _('Bangladéš'), + self::CODE_BGR => _('Bulharsko'), + self::CODE_BHR => _('Bahrajn'), + self::CODE_BHS => _('Bahamy'), + self::CODE_BIH => _('Bosna a Hercegovina'), + self::CODE_BES => _('Karibské Nizozemsko'), + self::CODE_BLR => _('Bělorusko'), + self::CODE_BLZ => _('Belize'), + self::CODE_BMU => _('Bermudy'), + self::CODE_BOL => _('Bolívie'), + self::CODE_BRA => _('Brazílie'), + self::CODE_BRB => _('Barbados'), + self::CODE_BRN => _('Brunej Darussalam'), + self::CODE_BTN => _('Bhútán'), + self::CODE_BVT => _('Bouvetův ostrov'), + self::CODE_BWA => _('Botswana'), + self::CODE_CAF => _('Středoafrická republika'), + self::CODE_CAN => _('Kanada'), + self::CODE_CCK => _('Kokosové ostrovy'), + self::CODE_CHE => _('Švýcarsko'), + self::CODE_CHL => _('Chile'), + self::CODE_CHN => _('Čína'), + self::CODE_CIV => _('Pobřeží slonoviny'), + self::CODE_CMR => _('Kamerun'), + self::CODE_COD => _('Kongo, demokratická republika'), + self::CODE_COG => _('Kongo'), + self::CODE_COK => _('Cookovy ostrovy'), + self::CODE_COL => _('Kolumbie'), + self::CODE_COM => _('Komory'), + self::CODE_CPV => _('Kapverdy'), + self::CODE_CRI => _('Kostarika'), + self::CODE_CUB => _('Kuba'), + self::CODE_CUW => _('Curaçao'), + self::CODE_CXR => _('Vánoční ostrov'), + self::CODE_CYM => _('Kajmanské ostrovy'), + self::CODE_CYP => _('Kypr'), + self::CODE_CZE => _('Česko'), + self::CODE_DEU => _('Německo'), + self::CODE_DJI => _('Džibutsko'), + self::CODE_DMA => _('Dominika'), + self::CODE_DNK => _('Dánsko'), + self::CODE_DOM => _('Dominikánská republika'), + self::CODE_DZA => _('Alžírsko'), + self::CODE_ECU => _('Ekvádor'), + self::CODE_EGY => _('Egypt'), + self::CODE_ERI => _('Eritrea'), + self::CODE_ESH => _('Západní Sahara'), + self::CODE_ESP => _('Španělsko'), + self::CODE_EST => _('Estonsko'), + self::CODE_ETH => _('Etiopie'), + self::CODE_FIN => _('Finsko'), + self::CODE_FJI => _('Fidži'), + self::CODE_FLK => _('Falklandy (Malvíny)'), + self::CODE_FRA => _('Francie'), + self::CODE_FRO => _('Faerské ostrovy'), + self::CODE_FSM => _('Mikronésie'), + self::CODE_GAB => _('Gabon'), + self::CODE_GBR => _('Velká Británie'), + self::CODE_GEO => _('Gruzie'), + self::CODE_GGY => _('Guernsey'), + self::CODE_GHA => _('Ghana'), + self::CODE_GIB => _('Gibraltar'), + self::CODE_GIN => _('Guinea'), + self::CODE_GLP => _('Guadeloupe'), + self::CODE_GMB => _('Gambie'), + self::CODE_GNB => _('Guinea-Bissau'), + self::CODE_GNQ => _('Rovníková Guinea'), + self::CODE_GRC => _('Řecko'), + self::CODE_GRD => _('Grenada'), + self::CODE_GRL => _('Grónsko'), + self::CODE_GTM => _('Guatemala'), + self::CODE_GUF => _('Francouzská Guyana'), + self::CODE_GUM => _('Guam'), + self::CODE_GUY => _('Guyana'), + self::CODE_HKG => _('Hongkong'), + self::CODE_HMD => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND => _('Honduras'), + self::CODE_HRV => _('Chorvatsko'), + self::CODE_HTI => _('Haiti'), + self::CODE_HUN => _('Maďarsko'), + self::CODE_IDN => _('Indonésie'), + self::CODE_IMN => _('Ostrov Man'), + self::CODE_IND => _('Indie'), + self::CODE_IOT => _('Britské indickooceánské území'), + self::CODE_IRL => _('Irsko'), + self::CODE_IRN => _('Írán'), + self::CODE_IRQ => _('Irák'), + self::CODE_ISL => _('Island'), + self::CODE_ISR => _('Izrael'), + self::CODE_ITA => _('Itálie'), + self::CODE_JAM => _('Jamajka'), + self::CODE_JEY => _('Jersey'), + self::CODE_JOR => _('Jordánsko'), + self::CODE_JPN => _('Japonsko'), + self::CODE_KAZ => _('Kazachstán'), + self::CODE_KEN => _('Keňa'), + self::CODE_KGZ => _('Kyrgyzstán'), + self::CODE_KHM => _('Kambodža'), + self::CODE_KIR => _('Kiribati'), + self::CODE_KNA => _('Svatý Kryštof a Nevis'), + self::CODE_KOR => _('Jižní Korea'), + self::CODE_KWT => _('Kuvajt'), + self::CODE_LAO => _('Laos'), + self::CODE_LBN => _('Libanon'), + self::CODE_LBR => _('Libérie'), + self::CODE_LBY => _('Libye'), + self::CODE_LCA => _('Svatá Lucie'), + self::CODE_LIE => _('Lichtenštejnsko'), + self::CODE_LKA => _('Srí Lanka'), + self::CODE_LSO => _('Lesotho'), + self::CODE_LTU => _('Litva'), + self::CODE_LUX => _('Lucembursko'), + self::CODE_LVA => _('Lotyšsko'), + self::CODE_MAC => _('Macao'), + self::CODE_MAF => _('Svatý Martin (Francie)'), + self::CODE_MAR => _('Maroko'), + self::CODE_MCO => _('Monako'), + self::CODE_MDA => _('Moldavsko'), + self::CODE_MDG => _('Madagaskar'), + self::CODE_MDV => _('Maledivy'), + self::CODE_MEX => _('Mexiko'), + self::CODE_MHL => _('Marshallovy ostrovy'), + self::CODE_MKD => _('Makedonie'), + self::CODE_MLI => _('Mali'), + self::CODE_MLT => _('Malta'), + self::CODE_MMR => _('Myanmar'), + self::CODE_MNE => _('Černá Hora'), + self::CODE_MNG => _('Mongolsko'), + self::CODE_MNP => _('Severní Mariany'), + self::CODE_MOZ => _('Mosambik'), + self::CODE_MRT => _('Mauritánie'), + self::CODE_MSR => _('Montserrat'), + self::CODE_MTQ => _('Martinik'), + self::CODE_MUS => _('Mauricius'), + self::CODE_MWI => _('Malawi'), + self::CODE_MYS => _('Malajsie'), + self::CODE_MYT => _('Mayotte'), + self::CODE_NAM => _('Namibie'), + self::CODE_NCL => _('Nová Kaledonie'), + self::CODE_NER => _('Niger'), + self::CODE_NFK => _('Norfolk'), + self::CODE_NGA => _('Nigérie'), + self::CODE_NIC => _('Nikaragua'), + self::CODE_NIU => _('Niue'), + self::CODE_NLD => _('Nizozemsko'), + self::CODE_NOR => _('Norsko'), + self::CODE_NPL => _('Nepál'), + self::CODE_NRU => _('Nauru'), + self::CODE_NZL => _('Nový Zéland'), + self::CODE_OMN => _('Omán'), + self::CODE_PAK => _('Pákistán'), + self::CODE_PAN => _('Panama'), + self::CODE_PCN => _('Pitcairn'), + self::CODE_PER => _('Peru'), + self::CODE_PHL => _('Filipíny'), + self::CODE_PLW => _('Palau'), + self::CODE_PNG => _('Papua Nová Guinea'), + self::CODE_POL => _('Polsko'), + self::CODE_PRI => _('Portoriko'), + self::CODE_PRK => _('Severní Korea'), + self::CODE_PRT => _('Portugalsko'), + self::CODE_PRY => _('Paraguay'), + self::CODE_PSE => _('Palestina'), + self::CODE_PYF => _('Francouzská Polynésie'), + self::CODE_QAT => _('Katar'), + self::CODE_REU => _('Réunion'), + self::CODE_ROU => _('Rumunsko'), + self::CODE_RUS => _('Rusko'), + self::CODE_RWA => _('Rwanda'), + self::CODE_SAU => _('Saúdská Arábie'), + self::CODE_SDN => _('Súdán'), + self::CODE_SEN => _('Senegal'), + self::CODE_SGP => _('Singapur'), + self::CODE_SGS => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN => _('Svatá Helena'), + self::CODE_SJM => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB => _('Šalamounovy ostrovy'), + self::CODE_SLE => _('Sierra Leone'), + self::CODE_SLV => _('Salvador'), + self::CODE_SMR => _('San Marino'), + self::CODE_SOM => _('Somálsko'), + self::CODE_SPM => _('Saint Pierre a Miquelon'), + self::CODE_SRB => _('Srbsko'), + self::CODE_SSU => _('Jižní Súdán'), + self::CODE_STP => _('Svatý Tomáš'), + self::CODE_SUR => _('Surinam'), + self::CODE_SVK => _('Slovensko'), + self::CODE_SVN => _('Slovinsko'), + self::CODE_SWE => _('Švédsko'), + self::CODE_SWZ => _('Svazijsko'), + self::CODE_SXM => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC => _('Seychely'), + self::CODE_SYR => _('Sýrie'), + self::CODE_TCA => _('Turks a Caicos'), + self::CODE_TCD => _('Čad'), + self::CODE_TGO => _('Togo'), + self::CODE_THA => _('Thajsko'), + self::CODE_TJK => _('Tádžikistán'), + self::CODE_TKL => _('Tokelau'), + self::CODE_TKM => _('Turkmenistán'), + self::CODE_TLS => _('Východní Timor'), + self::CODE_TON => _('Tonga'), + self::CODE_TTO => _('Trinidad a Tobago'), + self::CODE_TUN => _('Tunisko'), + self::CODE_TUR => _('Turecko'), + self::CODE_TUV => _('Tuvalu'), + self::CODE_TWN => _('Tchaj-wan'), + self::CODE_TZA => _('Tanzanie'), + self::CODE_UGA => _('Uganda'), + self::CODE_UKR => _('Ukrajina'), + self::CODE_UMI => _('Menší odlehlé ostrovy USA'), + self::CODE_URY => _('Uruguay'), + self::CODE_USA => _('Spojené státy'), + self::CODE_UZB => _('Uzbekistán'), + self::CODE_VAT => _('Vatikán'), + self::CODE_VCT => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN => _('Venezuela'), + self::CODE_VGB => _('Britské Panenské ostrovy'), + self::CODE_VIR => _('Americké Panenské ostrovy'), + self::CODE_VNM => _('Vietnam'), + self::CODE_VUT => _('Vanuatu'), + self::CODE_WLF => _('Wallis a Futuna'), + self::CODE_WSM => _('Samoa'), + self::CODE_XKX => _('Kosovo'), + self::CODE_YEM => _('Jemen'), + self::CODE_ZAF => _('Jihoafrická republika'), + self::CODE_ZMB => _('Zambie'), + self::CODE_ZWE => _('Zimbabwe'), + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/math.php b/tests/PHPStan/Analyser/data/math.php deleted file mode 100644 index 99a364ab09..0000000000 --- a/tests/PHPStan/Analyser/data/math.php +++ /dev/null @@ -1,112 +0,0 @@ -', 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('0', $rangeFiveBoth - $rangeFiveBoth); - - assertType('int<-10, max>', $rangeFiveBoth + $rangeFiveLeft); - assertType('int', $rangeFiveBoth - $rangeFiveLeft); - - assertType('int', $rangeFiveBoth + $rangeFiveRight); - assertType('int', $rangeFiveBoth - $rangeFiveRight); - - assertType('int<-10, max>', $rangeFiveLeft + $rangeFiveBoth); - assertType('int<0, 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); - } - -} diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php74.php b/tests/PHPStan/Analyser/data/mb-str-split-php74.php new file mode 100644 index 0000000000..840010c7c3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-str-split-php74.php @@ -0,0 +1,94 @@ +|false', $mbStrSplitConstantStringWithoutDefinedParameters); + + $mbStrSplitConstantStringWithoutDefinedSplitLength = mb_str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithoutDefinedSplitLength); + + $mbStrSplitStringWithoutDefinedSplitLength = mb_str_split($string); + assertType('list', $mbStrSplitStringWithoutDefinedSplitLength); + + $mbStrSplitConstantStringWithOneSplitLength = mb_str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithOneSplitLength); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength = mb_str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); + + $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); + assertType('list|false', $mbStrSplitConstantStringWithInvalidSplitLengthType); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('list|false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + + $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); + assertType('list|false', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); + assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); + assertType('list|false', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); + assertType('list|false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); + assertType('list|false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); + assertType('list|false', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); + assertType('list|false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); + assertType('list|false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + } +} diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php80.php b/tests/PHPStan/Analyser/data/mb-str-split-php80.php new file mode 100644 index 0000000000..9f5d0def9c --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-str-split-php80.php @@ -0,0 +1,115 @@ +', $mbStrSplitConstantStringWithoutDefinedParameters); + + $mbStrSplitConstantStringWithoutDefinedSplitLength = mb_str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithoutDefinedSplitLength); + + $mbStrSplitStringWithoutDefinedSplitLength = mb_str_split($string); + assertType('list', $mbStrSplitStringWithoutDefinedSplitLength); + + $mbStrSplitConstantStringWithOneSplitLength = mb_str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithOneSplitLength); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength = mb_str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLength); + + $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + + $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); + assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString, + int $integer, + ):void { + assertType('list', mb_str_split($string)); + assertType('non-empty-list', mb_str_split($nonEmptyString)); + assertType('non-empty-list', mb_str_split($nonFalsyString)); + + assertType('list', mb_str_split($string, $integer)); + assertType('non-empty-list', mb_str_split($nonEmptyString, $integer)); + assertType('non-empty-list', mb_str_split($nonFalsyString, $integer)); + } +} diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php82.php b/tests/PHPStan/Analyser/data/mb-str-split-php82.php new file mode 100644 index 0000000000..0f905fe37a --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-str-split-php82.php @@ -0,0 +1,113 @@ +', $mbStrSplitConstantStringWithoutDefinedParameters); + + $mbStrSplitConstantStringWithoutDefinedSplitLength = mb_str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithoutDefinedSplitLength); + + $mbStrSplitStringWithoutDefinedSplitLength = mb_str_split($string); + assertType('list', $mbStrSplitStringWithoutDefinedSplitLength); + + $mbStrSplitConstantStringWithOneSplitLength = mb_str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithOneSplitLength); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength = mb_str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLength); + + $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + + $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); + assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + int $integer, + ):void { + assertType('list', mb_str_split($string)); + assertType('non-empty-list', mb_str_split($nonEmptyString)); + assertType('non-empty-list', mb_str_split($nonFalsyString)); + + assertType('list', mb_str_split($string, $integer)); + assertType('non-empty-list', mb_str_split($nonEmptyString, $integer)); + assertType('non-empty-list', mb_str_split($nonFalsyString, $integer)); + } +} diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php73.php b/tests/PHPStan/Analyser/data/mb-strlen-php73.php new file mode 100644 index 0000000000..45fad0364b --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php73.php @@ -0,0 +1,56 @@ += 7.3 + +namespace MbStrlenPhp73; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp73 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('false', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5|false', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6|false', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|6|8|false', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|18|24|false', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3|false', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('false', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} + diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php8.php b/tests/PHPStan/Analyser/data/mb-strlen-php8.php new file mode 100644 index 0000000000..3fb7f73706 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php8.php @@ -0,0 +1,55 @@ += 8.0 + +namespace MbStrlenPhp8; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp8 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|6|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|18|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php82.php b/tests/PHPStan/Analyser/data/mb-strlen-php82.php new file mode 100644 index 0000000000..7424e938a4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php82.php @@ -0,0 +1,57 @@ += 8.2 + +declare(strict_types=1); + +namespace MbStrlenPhp82; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp82 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php b/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php deleted file mode 100644 index 0ba0e9ab4e..0000000000 --- a/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php +++ /dev/null @@ -1,21 +0,0 @@ -', mb_substitute_character()); -\PHPStan\Testing\assertType('true', mb_substitute_character('')); -\PHPStan\Testing\assertType('false', mb_substitute_character(null)); -\PHPStan\Testing\assertType('true', mb_substitute_character('none')); -\PHPStan\Testing\assertType('true', mb_substitute_character('long')); -\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); -\PHPStan\Testing\assertType('false', mb_substitute_character('foo')); -\PHPStan\Testing\assertType('true', mb_substitute_character('123')); -\PHPStan\Testing\assertType('true', mb_substitute_character('123.4')); -\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); -\PHPStan\Testing\assertType('false', mb_substitute_character(0x10FFFF)); -\PHPStan\Testing\assertType('false', mb_substitute_character(-1)); -\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000)); -\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); -\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass())); -\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {})); -\PHPStan\Testing\assertType('true', mb_substitute_character(rand(0xD800, 0xDFFF))); -\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); -\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php index c0dff8a645..254d321d7f 100644 --- a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php +++ b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php @@ -5,7 +5,7 @@ use SomeNamespace\Amet as Dolor; use SomeNamespace\Consecteur; -class FooInheritDocChild extends Foo +class FooInheritDocChildWithoutCurly extends Foo { /** diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php new file mode 100644 index 0000000000..5a44bfa784 --- /dev/null +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php @@ -0,0 +1,174 @@ +doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } + } + + /** + * @phan-return self[] + */ + public function doBar(): array + { + + } + + public function returnParent(): parent + { + + } + + /** + * @phan-return parent + */ + public function returnPhpDocParent() + { + + } + + /** + * @phan-return NULL[] + */ + public function returnNulls(): array + { + + } + + public function returnObject(): object + { + + } + + public function phpDocVoidMethod(): self + { + + } + + public function phpDocVoidMethodFromInterface(): self + { + + } + + public function phpDocVoidParentMethod(): self + { + + } + + public function phpDocWithoutCurlyBracesVoidParentMethod(): self + { + + } + + /** + * @phan-return string[] + */ + public function returnsStringArray(): array + { + + } + + private function privateMethodWithPhpDoc() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/data/minmax-arrays.php deleted file mode 100644 index 0f9ca3aede..0000000000 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ /dev/null @@ -1,152 +0,0 @@ - 0) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('false', min($ints)); - assertType('false', max($ints)); - } - if (count($ints) >= 1) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('false', min($ints)); - assertType('false', max($ints)); - } - if (count($ints) >= 2) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); - } - if (count($ints) <= 0) { - assertType('false', min($ints)); - assertType('false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } - if (count($ints) < 1) { - assertType('false', min($ints)); - assertType('false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } - if (count($ints) < 2) { - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } -} - -/** - * @param int[] $ints - */ -function dummy3(array $ints): void -{ - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); -} - - -function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void -{ - assertType('array(0 => DateTimeInterface, ?1 => DateTimeInterface)', array_filter([$dateA, $dateB])); - assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); - assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); - assertType('array(?0 => DateTimeInterface)', array_filter([$dateB])); - assertType('DateTimeInterface|false', min(array_filter([$dateB]))); - assertType('DateTimeInterface|false', max(array_filter([$dateB]))); -} - -function dummy5(int $i, int $j): void -{ - assertType('array(?0 => int|int<1, max>, ?1 => int|int<1, max>)', array_filter([$i, $j])); - assertType('array(1 => true)', array_filter([false, true])); -} - -function dummy6(string $s, string $t): void { - assertType('array(?0 => non-empty-string, ?1 => non-empty-string)', array_filter([$s, $t])); -} - -class HelloWorld -{ - public function setRange(int $range): void - { - if ($range < 0) { - return; - } - assertType('int<0, 100>', min($range, 100)); - assertType('int<0, 100>', min(100, $range)); - } - - public function setRange2(int $range): void - { - if ($range > 100) { - return; - } - assertType('int<0, 100>', max($range, 0)); - assertType('int<0, 100>', max(0, $range)); - } - - public function boundRange(): void - { - /** - * @var int<1, 6> $range - */ - $range = getFoo(); - - assertType('int<1, 4>', min($range, 4)); - assertType('int<4, 6>', max(4, $range)); - } -} diff --git a/tests/PHPStan/Analyser/data/model-mixin.php b/tests/PHPStan/Analyser/data/model-mixin.php deleted file mode 100644 index 69bd29565a..0000000000 --- a/tests/PHPStan/Analyser/data/model-mixin.php +++ /dev/null @@ -1,30 +0,0 @@ -= 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()); -}; diff --git a/tests/PHPStan/Analyser/data/modulo-operator.php b/tests/PHPStan/Analyser/data/modulo-operator.php deleted file mode 100644 index 6e432eb259..0000000000 --- a/tests/PHPStan/Analyser/data/modulo-operator.php +++ /dev/null @@ -1,48 +0,0 @@ - $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); - } -} diff --git a/tests/PHPStan/Analyser/data/native-types.php b/tests/PHPStan/Analyser/data/native-types.php deleted file mode 100644 index 56da164362..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&nonEmpty', $array); - assertNativeType('array&nonEmpty', $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('array&nonEmpty', $array); - assertNativeType('array&nonEmpty', $array); - - assertType('string', $key); - assertNativeType('(int|string)', $key); - - assertType('int', $i); - // assertNativeType('mixed', $i); - - assertType('string', $s); - // assertNativeType('mixed', $s); - } - } - - /** - * @param \DateTimeImmutable $date - */ - public function doIfElse(\DateTimeInterface $date): void - { - if ($date instanceof \DateTimeInterface) { - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeInterface::class, $date); - } else { - assertType('*NEVER*', $date); - assertNativeType('*NEVER*', $date); - } - - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeInterface::class, $date); - - if ($date instanceof \DateTimeImmutable) { - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeImmutable::class, $date); - } else { - assertType('*NEVER*', $date); - assertNativeType('DateTime', $date); - } - - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeImmutable::class, $date); // could be DateTimeInterface - - if ($date instanceof \DateTime) { - - } - } - -} - -/** - * @param Foo $foo - * @param \DateTimeImmutable $dateTime - * @param \DateTimeImmutable $dateTimeMutable - * @param string $nullableString - * @param string|null $nonNullableString - */ -function fooFunction( - $foo, - \DateTimeInterface $dateTime, - \DateTime $dateTimeMutable, - ?string $nullableString, - string $nonNullableString -): void -{ - assertType(Foo::class, $foo); - assertNativeType('mixed', $foo); - - assertType(\DateTimeImmutable::class, $dateTime); - assertNativeType(\DateTimeInterface::class, $dateTime); - - assertType(\DateTime::class, $dateTimeMutable); - assertNativeType(\DateTime::class, $dateTimeMutable); - - assertType('string|null', $nullableString); - assertNativeType('string|null', $nullableString); - - assertType('string', $nonNullableString); - assertNativeType('string', $nonNullableString); -} diff --git a/tests/PHPStan/Analyser/data/nested-functions.php b/tests/PHPStan/Analyser/data/nested-functions.php index 1d12b75157..b33ed3150a 100644 --- a/tests/PHPStan/Analyser/data/nested-functions.php +++ b/tests/PHPStan/Analyser/data/nested-functions.php @@ -12,8 +12,7 @@ public function doFoo(): self } -function () { - $foo = new Foo(); +function (Foo $foo) { $foo->doFoo() ->doFoo() ->doFoo() diff --git a/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php 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/non-empty-array.php b/tests/PHPStan/Analyser/data/non-empty-array.php deleted file mode 100644 index 316620e1af..0000000000 --- a/tests/PHPStan/Analyser/data/non-empty-array.php +++ /dev/null @@ -1,55 +0,0 @@ - $arrayOfStrings - * @param non-empty-list<\stdClass> $listOfStd - * @param non-empty-list<\stdClass> $listOfStd2 - * @param non-empty-list $invalidList - */ - public function doFoo( - array $array, - array $list, - array $arrayOfStrings, - array $listOfStd, - $listOfStd2, - array $invalidList, - $invalidList2 - ): void - { - assertType('array&nonEmpty', $array); - assertType('array&nonEmpty', $list); - assertType('array&nonEmpty', $arrayOfStrings); - assertType('array&nonEmpty', $listOfStd); - assertType('array&nonEmpty', $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('array&nonEmpty', array_combine($array, $array)); - assertType('array&nonEmpty', array_combine($list, $list)); - - assertType('array&nonEmpty', array_merge($array)); - assertType('array&nonEmpty', array_merge([], $array)); - assertType('array&nonEmpty', array_merge($array, [])); - assertType('array&nonEmpty', array_merge($array, $array)); - - assertType('array&nonEmpty', array_flip($array)); - assertType('array&nonEmpty', array_flip($stringArray)); - } -} diff --git a/tests/PHPStan/Analyser/data/non-empty-string.php b/tests/PHPStan/Analyser/data/non-empty-string.php deleted file mode 100644 index c9b1b8cfa0..0000000000 --- a/tests/PHPStan/Analyser/data/non-empty-string.php +++ /dev/null @@ -1,373 +0,0 @@ - 0) { - assertType('non-empty-string', $s); - return; - } - - assertType('\'\'', $s); - } - - public function doBar3(string $s): void - { - if (strlen($s) >= 1) { - assertType('non-empty-string', $s); - return; - } - - assertType('\'\'', $s); - } - - public function doFoo5(string $s): void - { - if (0 === strlen($s)) { - return; - } - - assertType('non-empty-string', $s); - } - - public function doBar4(string $s): void - { - if (0 < strlen($s)) { - assertType('non-empty-string', $s); - return; - } - - assertType('\'\'', $s); - } - - public function doBar5(string $s): void - { - if (1 <= strlen($s)) { - assertType('non-empty-string', $s); - return; - } - - assertType('\'\'', $s); - } - - public function doFoo3(string $s): void - { - if ($s) { - assertType('non-empty-string', $s); - } else { - assertType('\'\'|\'0\'', $s); - } - } - - /** - * @param non-empty-string $s - */ - public function doFoo4(string $s): void - { - assertType('array&nonEmpty', explode($s, 'foo')); - } - - /** - * @param non-empty-string $s - */ - public function doWithNumeric(string $s): void - { - if (!is_numeric($s)) { - return; - } - - assertType('non-empty-string', $s); - } - - public function doEmpty(string $s): void - { - if (empty($s)) { - return; - } - - assertType('non-empty-string', $s); - } - - public function doEmpty2(string $s): void - { - if (!empty($s)) { - assertType('non-empty-string', $s); - } - } - - /** - * @param non-empty-string $nonEmpty - * @param positive-int $positiveInt - * @param 1|2|3 $postiveRange - * @param -1|-2|-3 $negativeRange - */ - public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void - { - assertType('string', substr($s, 5)); - - assertType('string', substr($s, -5)); - assertType('non-empty-string', substr($nonEmpty, -5)); - assertType('non-empty-string', substr($nonEmpty, $negativeRange)); - - assertType('string', substr($s, 0, 5)); - assertType('non-empty-string', substr($nonEmpty, 0, 5)); - assertType('non-empty-string', substr($nonEmpty, 0, $postiveRange)); - - assertType('string', substr($nonEmpty, 0, -5)); - - assertType('string', substr($s, 0, $positiveInt)); - assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt)); - } -} - -class ImplodingStrings -{ - - /** - * @param array $commonStrings - */ - public function doFoo(string $s, array $commonStrings): void - { - assertType('string', implode($s, $commonStrings)); - assertType('string', implode(' ', $commonStrings)); - assertType('string', implode('', $commonStrings)); - assertType('string', implode($commonStrings)); - } - - /** - * @param non-empty-array $nonEmptyArrayWithStrings - */ - public function doFoo2(string $s, array $nonEmptyArrayWithStrings): void - { - assertType('string', implode($s, $nonEmptyArrayWithStrings)); - assertType('string', implode('', $nonEmptyArrayWithStrings)); - assertType('non-empty-string', implode(' ', $nonEmptyArrayWithStrings)); - assertType('string', implode($nonEmptyArrayWithStrings)); - } - - /** - * @param array $arrayWithNonEmptyStrings - */ - public function doFoo3(string $s, array $arrayWithNonEmptyStrings): void - { - assertType('string', implode($s, $arrayWithNonEmptyStrings)); - assertType('string', implode('', $arrayWithNonEmptyStrings)); - assertType('string', implode(' ', $arrayWithNonEmptyStrings)); - assertType('string', implode($arrayWithNonEmptyStrings)); - } - - /** - * @param non-empty-array $nonEmptyArrayWithNonEmptyStrings - */ - public function doFoo4(string $s, array $nonEmptyArrayWithNonEmptyStrings): void - { - assertType('non-empty-string', implode($s, $nonEmptyArrayWithNonEmptyStrings)); - assertType('non-empty-string', implode('', $nonEmptyArrayWithNonEmptyStrings)); - assertType('non-empty-string', implode(' ', $nonEmptyArrayWithNonEmptyStrings)); - assertType('non-empty-string', implode($nonEmptyArrayWithNonEmptyStrings)); - } - - public function sayHello(): void - { - // coming from issue #5291 - $s = array(1, 2); - - assertType('non-empty-string', implode("a", $s)); - } - - /** - * @param non-empty-string $glue - */ - public function nonE($glue, array $a) - { - // coming from issue #5291 - if (empty($a)) { - return "xyz"; - } - - assertType('non-empty-string', implode($glue, $a)); - } - - public function sayHello2(): void - { - // coming from issue #5291 - $s = array(1, 2); - - assertType('non-empty-string', join("a", $s)); - } - - /** - * @param non-empty-string $glue - */ - public function nonE2($glue, array $a) - { - // coming from issue #5291 - if (empty($a)) { - return "xyz"; - } - - assertType('non-empty-string', join($glue, $a)); - } - -} - -class LiteralString -{ - - function x(string $tableName, string $original): void - { - assertType('non-empty-string', "from `$tableName`"); - } - - /** - * @param non-empty-string $nonEmpty - */ - function concat(string $s, string $nonEmpty): void - { - assertType('string', $s . ''); - assertType('non-empty-string', $nonEmpty . ''); - assertType('non-empty-string', $nonEmpty . $s); - } - -} - -class GeneralizeConstantStringType -{ - - /** - * @param array $a - * @param non-empty-string $s - */ - public function doFoo(array $a, string $s): void - { - $a[$s] = 2; - - // there might be non-empty-string that becomes a number instead - assertType('array&nonEmpty', $a); - } - - /** - * @param array $a - * @param non-empty-string $s - */ - public function doFoo2(array $a, string $s): void - { - $a[''] = 2; - assertType('array&nonEmpty', $a); - } - -} - -class MoreNonEmptyStringFunctions -{ - - /** - * @param non-empty-string $nonEmpty - * @param '1'|'2'|'5'|'10' $constUnion - */ - public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUnion) - { - assertType('string', addslashes($s)); - assertType('non-empty-string', addslashes($nonEmpty)); - assertType('string', addcslashes($s)); - assertType('non-empty-string', addcslashes($nonEmpty)); - - assertType('string', escapeshellarg($s)); - assertType('non-empty-string', escapeshellarg($nonEmpty)); - assertType('string', escapeshellcmd($s)); - assertType('non-empty-string', escapeshellcmd($nonEmpty)); - - assertType('string', strtoupper($s)); - assertType('non-empty-string', strtoupper($nonEmpty)); - assertType('string', strtolower($s)); - assertType('non-empty-string', strtolower($nonEmpty)); - assertType('string', mb_strtoupper($s)); - assertType('non-empty-string', mb_strtoupper($nonEmpty)); - assertType('string', mb_strtolower($s)); - assertType('non-empty-string', mb_strtolower($nonEmpty)); - assertType('string', lcfirst($s)); - assertType('non-empty-string', lcfirst($nonEmpty)); - assertType('string', ucfirst($s)); - assertType('non-empty-string', ucfirst($nonEmpty)); - assertType('string', ucwords($s)); - assertType('non-empty-string', ucwords($nonEmpty)); - assertType('string', htmlspecialchars($s)); - assertType('non-empty-string', htmlspecialchars($nonEmpty)); - assertType('string', htmlentities($s)); - assertType('non-empty-string', htmlentities($nonEmpty)); - - assertType('string', urlencode($s)); - assertType('non-empty-string', urlencode($nonEmpty)); - assertType('string', urldecode($s)); - assertType('non-empty-string', urldecode($nonEmpty)); - assertType('string', rawurlencode($s)); - assertType('non-empty-string', rawurlencode($nonEmpty)); - assertType('string', rawurldecode($s)); - assertType('non-empty-string', rawurldecode($nonEmpty)); - - assertType('string', sprintf($s)); - assertType('non-empty-string', sprintf($nonEmpty)); - assertType('string', vsprintf($s, [])); - assertType('non-empty-string', vsprintf($nonEmpty, [])); - - assertType('0', strlen('')); - assertType('5', strlen('hallo')); - assertType('int<0, 1>', strlen($bool)); - assertType('int<1, max>', strlen($i)); - assertType('int<0, max>', strlen($s)); - assertType('int<1, max>', strlen($nonEmpty)); - assertType('int<1, 2>', strlen($constUnion)); - - assertType('non-empty-string', str_pad($nonEmpty, 0)); - assertType('non-empty-string', str_pad($nonEmpty, 1)); - assertType('string', str_pad($s, 0)); - assertType('non-empty-string', str_pad($s, 1)); - - assertType('non-empty-string', str_repeat($nonEmpty, 1)); - assertType('\'\'', str_repeat($nonEmpty, 0)); - assertType('string', str_repeat($nonEmpty, $i)); - assertType('\'\'', str_repeat($s, 0)); - assertType('string', str_repeat($s, 1)); - assertType('string', str_repeat($s, $i)); - } - -} diff --git a/tests/PHPStan/Analyser/data/number_format.php b/tests/PHPStan/Analyser/data/number_format.php deleted file mode 100644 index 271e501825..0000000000 --- a/tests/PHPStan/Analyser/data/number_format.php +++ /dev/null @@ -1,18 +0,0 @@ -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-this-extension.php b/tests/PHPStan/Analyser/data/parameter-closure-this-extension.php new file mode 100644 index 0000000000..31660060ef --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-this-extension.php @@ -0,0 +1,108 @@ +getName() === 'ParameterClosureThisExtension\testFunction' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestMethodParameterClosureThisExtension implements \PHPStan\Type\MethodParameterClosureThisExtension +{ + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === TestClass::class + && $methodReflection->getName() === 'methodWithClosure' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestStaticMethodParameterClosureThisExtension implements \PHPStan\Type\StaticMethodParameterClosureThisExtension +{ + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === TestClass::class + && $methodReflection->getName() === 'staticMethodWithClosure' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestContext +{ + public function contextMethod(): string + { + return 'context'; + } +} + +class TestClass +{ + public function methodWithClosure(callable $closure): void + { + } + + public static function staticMethodWithClosure(callable $closure): void + { + } +} + +/** + * @param callable $closure + */ +function testFunction(callable $closure): void +{ +} + +testFunction(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +$test = new TestClass(); +$test->methodWithClosure(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +TestClass::staticMethodWithClosure(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +testFunction(fn () => assertType('ParameterClosureThisExtension\TestContext', $this)); + +testFunction(static function () { + assertType('*ERROR*', $this); +}); + +testFunction(static fn () => assertType('*ERROR*', $this)); 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 deleted file mode 100644 index 62b2e4b910..0000000000 --- a/tests/PHPStan/Analyser/data/php74_functions.php +++ /dev/null @@ -1,33 +0,0 @@ - $data + * @return array + */ + public function someMethod(array $data): array + { + foreach ($data[self::FIELD_NOTES][self::SUBFIELD_NOTE] ?? [] as $index => $noteData) { + $noteTitle = $noteData[self::FIELD_TITLE] ?? null; + $noteSource = $noteData[self::FIELD_SOURCE] ?? null; + $noteBody = $noteData[self::FIELD_BODY] ?? null; + + if ($noteBody === null || trim($noteBody) === '') { + $data[self::FIELD_NOTES] = self::EMPTY_NOTE_BODY; + } + } + + if (isset($data[self::FIELD_NOTES][self::SUBFIELD_NOTE])) {} + + return $data; + } + +} diff --git a/tests/PHPStan/Analyser/data/predefined-constants-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 @@ +|false', preg_split('/-/', '1-2-3')); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); diff --git a/tests/PHPStan/Analyser/data/prestashop-xml-loader.php b/tests/PHPStan/Analyser/data/prestashop-xml-loader.php new file mode 100644 index 0000000000..37c9e729e3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/prestashop-xml-loader.php @@ -0,0 +1,97 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShopBundleInfiniteRunBug; + +class XmlLoader +{ + + protected $data_path; + + public function getEntityInfo($entity, $exists) + { + $info = [ + 'config' => [ + 'id' => '', + 'primary' => '', + 'class' => '', + 'sql' => '', + 'ordersql' => '', + 'image' => '', + 'null' => '', + ], + 'fields' => [], + ]; + + if (!$exists) { + return $info; + } + + $xml = @simplexml_load_file($this->data_path . $entity . '.xml', 'SimplexmlElement'); + if (!$xml) { + return $info; + } + + if ($xml->fields['id']) { + $info['config']['id'] = (string) $xml->fields['id']; + } + + if ($xml->fields['primary']) { + $info['config']['primary'] = (string) $xml->fields['primary']; + } + + if ($xml->fields['class']) { + $info['config']['class'] = (string) $xml->fields['class']; + } + + if ($xml->fields['sql']) { + $info['config']['sql'] = (string) $xml->fields['sql']; + } + + if ($xml->fields['ordersql']) { + $info['config']['ordersql'] = (string) $xml->fields['ordersql']; + } + + if ($xml->fields['null']) { + $info['config']['null'] = (string) $xml->fields['null']; + } + + if ($xml->fields['image']) { + $info['config']['image'] = (string) $xml->fields['image']; + } + + foreach ($xml->fields->field as $field) { + $column = (string) $field['name']; + $info['fields'][$column] = []; + if (isset($field['relation'])) { + $info['fields'][$column]['relation'] = (string) $field['relation']; + } + } + + return $info; + } + +} diff --git a/tests/PHPStan/Analyser/data/proc_get_status.php b/tests/PHPStan/Analyser/data/proc_get_status.php deleted file mode 100644 index 449b4fd0ff..0000000000 --- a/tests/PHPStan/Analyser/data/proc_get_status.php +++ /dev/null @@ -1,15 +0,0 @@ - string, \'pid\' => int, \'running\' => bool, \'signaled\' => bool, \'stopped\' => bool, \'exitcode\' => int, \'termsig\' => int, \'stopsig\' => int)', $status); -}; diff --git a/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php b/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php new file mode 100644 index 0000000000..9b6cc0925d --- /dev/null +++ b/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php @@ -0,0 +1,41 @@ +value); + } + /** @param \Closure(T|null): T $callback */ + public function onResolve2(\Closure $callback) : void{ + $r = $callback($this->value); + assertType('TValue (class ProcessCalledMethodInfiniteLoop\\Promise, argument)', $r); + + $callback($this->value); + } +} +class HelloWorld +{ + /** + * @template TValue + * @param \Generator, TValue|null, void> $async + */ + public function next(\Generator $async) : void{ + $async->next(); + if(!$async->valid()) return; + $promise = $async->current(); + $promise->onResolve(function($value) use ($async) : void{ + $async->send($value); + $this->next($async); + }); + } +} diff --git a/tests/PHPStan/Analyser/data/properties-defined.php b/tests/PHPStan/Analyser/data/properties-defined.php index 2ca412e875..cdc4cbb255 100644 --- a/tests/PHPStan/Analyser/data/properties-defined.php +++ b/tests/PHPStan/Analyser/data/properties-defined.php @@ -2,6 +2,7 @@ namespace PropertiesNamespace; +use AllowDynamicProperties; use DOMDocument; use SomeNamespace\Sit as Dolor; @@ -9,6 +10,7 @@ * @property-read int $readOnlyProperty * @property-read int $overriddenReadOnlyProperty */ +#[AllowDynamicProperties] class Bar extends DOMDocument { diff --git a/tests/PHPStan/Analyser/data/property-native-types.php b/tests/PHPStan/Analyser/data/property-native-types.php index 2892179613..87d990b96f 100644 --- a/tests/PHPStan/Analyser/data/property-native-types.php +++ b/tests/PHPStan/Analyser/data/property-native-types.php @@ -1,4 +1,4 @@ -= 7.4 +', 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))); - -assertType('int<-5, 5>', rand(-5, 5)); -assertType('int<0, max>', rand()); diff --git a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php deleted file mode 100644 index b657010861..0000000000 --- a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php +++ /dev/null @@ -1,79 +0,0 @@ - $genericClassName - */ -function testGetAttributes( - \ReflectionClass $reflectionClass, - \ReflectionMethod $reflectionMethod, - \ReflectionParameter $reflectionParameter, - \ReflectionProperty $reflectionProperty, - \ReflectionClassConstant $reflectionClassConstant, - \ReflectionFunction $reflectionFunction, - string $str, - string $className, - string $genericClassName -): void -{ - $classAll = $reflectionClass->getAttributes(); - $classAbc1 = $reflectionClass->getAttributes(Abc::class); - $classAbc2 = $reflectionClass->getAttributes(Abc::class, \ReflectionAttribute::IS_INSTANCEOF); - $classGCN = $reflectionClass->getAttributes($genericClassName); - $classCN = $reflectionClass->getAttributes($className); - $classStr = $reflectionClass->getAttributes($str); - $classNonsense = $reflectionClass->getAttributes("some random string"); - - assertType('array>', $classAll); - assertType('array>', $classAbc1); - assertType('array>', $classAbc2); - assertType('array>', $classGCN); - assertType('array>', $classCN); - assertType('array>', $classStr); - assertType('array>', $classNonsense); - - $methodAll = $reflectionMethod->getAttributes(); - $methodAbc = $reflectionMethod->getAttributes(Abc::class); - assertType('array>', $methodAll); - assertType('array>', $methodAbc); - - $paramAll = $reflectionParameter->getAttributes(); - $paramAbc = $reflectionParameter->getAttributes(Abc::class); - assertType('array>', $paramAll); - assertType('array>', $paramAbc); - - $propAll = $reflectionProperty->getAttributes(); - $propAbc = $reflectionProperty->getAttributes(Abc::class); - assertType('array>', $propAll); - assertType('array>', $propAbc); - - $constAll = $reflectionClassConstant->getAttributes(); - $constAbc = $reflectionClassConstant->getAttributes(Abc::class); - assertType('array>', $constAll); - assertType('array>', $constAbc); - - $funcAll = $reflectionFunction->getAttributes(); - $funcAbc = $reflectionFunction->getAttributes(Abc::class); - assertType('array>', $funcAll); - assertType('array>', $funcAbc); -} - -/** - * @param \ReflectionAttribute $ra - */ -function testNewInstance(\ReflectionAttribute $ra): void -{ - assertType('ReflectionAttribute', $ra); - $abc = $ra->newInstance(); - assertType(Abc::class, $abc); -} diff --git a/tests/PHPStan/Analyser/data/scope-constants-global.php b/tests/PHPStan/Analyser/data/scope-constants-global.php new file mode 100644 index 0000000000..56eba41c9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-constants-global.php @@ -0,0 +1,9 @@ + assertType('int', $nullable), + self::ALLOW_NULLABLE_INT => assertType('int|null', $nullable), + }; + } +} + + + diff --git a/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php b/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php new file mode 100644 index 0000000000..7062419000 --- /dev/null +++ b/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php @@ -0,0 +1,15 @@ +name instanceof Identifier && $bar->name instanceof Identifier) { - function () use ($call): void { - assertType('PhpParser\Node\Identifier', $call->name); - assertType('mixed', $bar->name); - }; - - assertType('PhpParser\Node\Identifier', $call->name); - } - } - - public function doBar(MethodCall $call, MethodCall $bar): void - { - if ($call->name instanceof Identifier && $bar->name instanceof Identifier) { - $a = 1; - function () use ($call, &$a): void { - assertType('PhpParser\Node\Identifier', $call->name); - assertType('mixed', $bar->name); - }; - - assertType('PhpParser\Node\Identifier', $call->name); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/sscanf.php b/tests/PHPStan/Analyser/data/sscanf.php deleted file mode 100644 index 2b7105f939..0000000000 --- a/tests/PHPStan/Analyser/data/sscanf.php +++ /dev/null @@ -1,6 +0,0 @@ -|false', $strSplitConstantStringWithoutDefinedParameters); + + $strSplitConstantStringWithoutDefinedSplitLength = str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithoutDefinedSplitLength); + + $strSplitStringWithoutDefinedSplitLength = str_split($string); + assertType('non-empty-list', $strSplitStringWithoutDefinedSplitLength); + + $strSplitConstantStringWithOneSplitLength = str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithOneSplitLength); + + $strSplitConstantStringWithGreaterSplitLengthThanStringLength = str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $strSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); + assertType('false', $strSplitConstantStringWithFailureSplitLength); + + $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); + assertType('non-empty-list|false', $strSplitConstantStringWithInvalidSplitLengthType); + + $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); + + $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + + } +} diff --git a/tests/PHPStan/Analyser/data/str-split-php80.php b/tests/PHPStan/Analyser/data/str-split-php80.php new file mode 100644 index 0000000000..7fe3a36ef9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-split-php80.php @@ -0,0 +1,59 @@ +', $strSplitConstantStringWithoutDefinedParameters); + + $strSplitConstantStringWithoutDefinedSplitLength = str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithoutDefinedSplitLength); + + $strSplitStringWithoutDefinedSplitLength = str_split($string); + assertType('non-empty-list', $strSplitStringWithoutDefinedSplitLength); + + $strSplitConstantStringWithOneSplitLength = str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithOneSplitLength); + + $strSplitConstantStringWithGreaterSplitLengthThanStringLength = str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $strSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); + assertType('*NEVER*', $strSplitConstantStringWithFailureSplitLength); + + $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); + + $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); + + $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + + } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + int $integer, + ):void { + assertType('non-empty-list', str_split($string)); + assertType('non-empty-list', str_split($nonEmptyString)); + assertType('non-empty-list', str_split($nonFalsyString)); + + assertType('non-empty-list', str_split($string, $integer)); + assertType('non-empty-list', str_split($nonEmptyString, $integer)); + assertType('non-empty-list', str_split($nonFalsyString, $integer)); + } +} diff --git a/tests/PHPStan/Analyser/data/str-split-php82.php b/tests/PHPStan/Analyser/data/str-split-php82.php new file mode 100644 index 0000000000..22720747e6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-split-php82.php @@ -0,0 +1,59 @@ +', $strSplitConstantStringWithoutDefinedParameters); + + $strSplitConstantStringWithoutDefinedSplitLength = str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithoutDefinedSplitLength); + + $strSplitStringWithoutDefinedSplitLength = str_split($string); + assertType('list', $strSplitStringWithoutDefinedSplitLength); + + $strSplitConstantStringWithOneSplitLength = str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithOneSplitLength); + + $strSplitConstantStringWithGreaterSplitLengthThanStringLength = str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $strSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); + assertType('*NEVER*', $strSplitConstantStringWithFailureSplitLength); + + $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); + + $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); + + $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + + } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + int $integer, + ):void { + assertType('list', str_split($string)); + assertType('non-empty-list', str_split($nonEmptyString)); + assertType('non-empty-list', str_split($nonFalsyString)); + + assertType('list', str_split($string, $integer)); + assertType('non-empty-list', str_split($nonEmptyString, $integer)); + assertType('non-empty-list', str_split($nonFalsyString, $integer)); + } +} diff --git a/tests/PHPStan/Analyser/data/strval.php b/tests/PHPStan/Analyser/data/strval.php deleted file mode 100644 index d85d37eec1..0000000000 --- a/tests/PHPStan/Analyser/data/strval.php +++ /dev/null @@ -1,93 +0,0 @@ - $class - */ -function strvalTest(string $string, string $class): void -{ - assertType('null', strval()); - assertType('\'foo\'', strval('foo')); - assertType('string', strval($string)); - assertType('\'\'', strval(null)); - assertType('\'\'', strval(false)); - assertType('\'1\'', strval(true)); - assertType('\'\'|\'1\'', strval(rand(0, 1) === 0)); - assertType('\'42\'', strval(42)); - assertType('string&numeric', strval(rand())); - assertType('string&numeric', strval(rand() * 0.1)); - assertType('string&numeric', strval(strval(rand()))); - assertType('class-string', strval($class)); - assertType('string', strval(new \Exception())); - assertType('*ERROR*', strval(new \stdClass())); -} - -function intvalTest(string $string): void -{ - assertType('null', intval()); - assertType('42', intval('42')); - assertType('0', intval('foo')); - assertType('int', intval($string)); - assertType('0', intval(null)); - assertType('0', intval(false)); - assertType('1', intval(true)); - assertType('0|1', intval(rand(0, 1) === 0)); - assertType('42', intval(42)); - assertType('int<0, max>', intval(rand())); - assertType('int', intval(rand() * 0.1)); - assertType('0', intval([])); - assertType('1', intval([null])); -} - -function floatvalTest(string $string): void -{ - assertType('null', floatval()); - assertType('3.14', floatval('3.14')); - assertType('0.0', floatval('foo')); - assertType('float', floatval($string)); - assertType('0.0', floatval(null)); - assertType('0.0', floatval(false)); - assertType('1.0', floatval(true)); - assertType('0.0|1.0', floatval(rand(0, 1) === 0)); - assertType('42.0', floatval(42)); - assertType('float', floatval(rand())); - assertType('float', floatval(rand() * 0.1)); - assertType('0.0', floatval([])); - assertType('1.0', floatval([null])); -} - -function boolvalTest(string $string): void -{ - assertType('null', boolval()); - assertType('false', boolval('')); - assertType('true', boolval('foo')); - assertType('bool', boolval($string)); - assertType('false', boolval(null)); - assertType('false', boolval(false)); - assertType('true', boolval(true)); - assertType('bool', boolval(rand(0, 1) === 0)); - assertType('true', boolval(42)); - assertType('bool', boolval(rand())); - assertType('bool', boolval(rand() * 0.1)); - assertType('false', boolval([])); - assertType('true', boolval([null])); - assertType('true', boolval(new \stdClass())); -} - -function arrayTest(array $a): void -{ - assertType('0|1', intval($a)); - assertType('0.0|1.0', floatval($a)); - assertType('bool', boolval($a)); -} - -/** @param non-empty-array $a */ -function nonEmptyArrayTest(array $a): void -{ - assertType('1', intval($a)); - assertType('1.0', floatval($a)); - assertType('true', boolval($a)); -} diff --git a/tests/PHPStan/Analyser/data/throws-tag-from-native-function-stub.php b/tests/PHPStan/Analyser/data/throws-tag-from-native-function-stub.php new file mode 100644 index 0000000000..c1cb5b40f2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/throws-tag-from-native-function-stub.php @@ -0,0 +1,21 @@ +doFoo(5)); +assertType('int', (new Bar)->notTypedFoo); diff --git a/tests/PHPStan/Analyser/data/trait-stubs.stub b/tests/PHPStan/Analyser/data/trait-stubs.stub new file mode 100644 index 0000000000..e3b314e362 --- /dev/null +++ b/tests/PHPStan/Analyser/data/trait-stubs.stub @@ -0,0 +1,17 @@ +pipeInto(User::class); +}; diff --git a/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon b/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon new file mode 100644 index 0000000000..4c33864671 --- /dev/null +++ b/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithBlock: false diff --git a/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon b/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon new file mode 100644 index 0000000000..971a1184f4 --- /dev/null +++ b/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon @@ -0,0 +1,2 @@ +parameters: + rememberPossiblyImpureFunctionValues: false diff --git a/tests/PHPStan/Analyser/dynamic-return-type.neon b/tests/PHPStan/Analyser/dynamic-return-type.neon index b8bd815fb4..e80f2018a0 100644 --- a/tests/PHPStan/Analyser/dynamic-return-type.neon +++ b/tests/PHPStan/Analyser/dynamic-return-type.neon @@ -27,3 +27,15 @@ services: class: PHPStan\Tests\FooGetSelf tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\ConditionalGetSingle + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\Bug7344DynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\Bug7391BDynamicStaticMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon index 7e42b64ac0..905063b2f6 100644 --- a/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon +++ b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon @@ -8,3 +8,14 @@ services: class: DynamicMethodThrowTypeExtension\StaticMethodThrowTypeExtension tags: - phpstan.dynamicStaticMethodThrowTypeExtension + + - + class: DynamicMethodThrowTypeExtensionNamedArgs\MethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + class: DynamicMethodThrowTypeExtensionNamedArgs\StaticMethodThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension + diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon new file mode 100644 index 0000000000..de0f92640b --- /dev/null +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -0,0 +1,7 @@ +# config for ExpressionTypeResolverExtensionTest +services: + - + class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension + diff --git a/tests/PHPStan/Analyser/nodeScopeResolverPhp7.neon b/tests/PHPStan/Analyser/nodeScopeResolverPhp7.neon new file mode 100644 index 0000000000..b41c80f2a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nodeScopeResolverPhp7.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 70400 # PHP 7.4 diff --git a/tests/PHPStan/Analyser/nodeScopeResolverPhp8.neon b/tests/PHPStan/Analyser/nodeScopeResolverPhp8.neon new file mode 100644 index 0000000000..0ed5d49f80 --- /dev/null +++ b/tests/PHPStan/Analyser/nodeScopeResolverPhp8.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80000 # PHP 8.0 diff --git a/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php b/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php new file mode 100644 index 0000000000..f50a903dbf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php @@ -0,0 +1,48 @@ +modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'+2 day' $modify + */ + public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param 'kewk'|'koko' $modify + */ + public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('false', $datetime->modify($modify)); + assertType('false', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'koko' $modify + */ + public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('(DateTime|false)', $datetime->modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php b/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php new file mode 100644 index 0000000000..d1d9af0558 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php @@ -0,0 +1,42 @@ += 8.3 + +declare(strict_types = 1); + +namespace DateTimeModifyReturnTypes83; + +use DateTime; +use DateTimeImmutable; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function modify(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'+2 day' $modify + */ + public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param 'kewk'|'koko' $modify + */ + public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('*NEVER*', $datetime->modify($modify)); + assertType('*NEVER*', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'koko' $modify + */ + public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/PDOStatement.php b/tests/PHPStan/Analyser/nsrt/PDOStatement.php new file mode 100644 index 0000000000..d9e930061e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/PDOStatement.php @@ -0,0 +1,22 @@ +fetchObject(Bar::class); + assertType('PDOStatement\Bar|false', $bar); + + $bar = $statement->fetchObject(); + assertType('stdClass|false', $bar); + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php new file mode 100644 index 0000000000..506f436c02 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -0,0 +1,217 @@ += 8.0 + +declare(strict_types = 1); + +namespace Abs; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function singleIntegerRange(int $int): void + { + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var positive-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var negative-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var non-negative-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var non-positive-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, max> $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<-123, 0> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<123, max> $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<123, 456> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<-456, -123> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int<-123, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + } + + public function multipleIntegerRanges(int $int): void + { + /** @var non-zero-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int|int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<-20, -10>|int<5, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -5>|int<10, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-25, -10>|int<5, 20> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -10>|int<20, 30> $int */ + assertType('int<10, 30>', abs($int)); + } + + public function constantInteger(int $int): void + { + /** @var 0 $int */ + assertType('0', abs($int)); + + /** @var 1 $int */ + assertType('1', abs($int)); + + /** @var -1 $int */ + assertType('1', abs($int)); + + assertType('123', abs(123)); + + assertType('123', abs(-123)); + } + + public function mixedIntegerUnion(int $int): void + { + /** @var 123|int<456, max> $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var int|-123 $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var -123|int<124, 125> $int */ + assertType('int<123, 125>', abs($int)); + + /** @var int<124, 125>|-123 $int */ + assertType('int<123, 125>', abs($int)); + } + + public function constantFloat(float $float): void + { + /** @var 0.0 $float */ + assertType('0.0', abs($float)); + + /** @var 1.0 $float */ + assertType('1.0', abs($float)); + + /** @var -1.0 $float */ + assertType('1.0', abs($float)); + + assertType('123.4', abs(123.4)); + + assertType('123.4', abs(-123.4)); + } + + public function string(string $string): void + { + /** @var string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var numeric-string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var '-1' $string */ + assertType('1', abs($string)); + + /** @var '-1'|'-2.0'|'3.0'|'4' $string */ + assertType('1|2.0|3.0|4', abs($string)); + + /** @var literal-string $string */ + assertType('float|int<0, max>', abs($string)); + + assertType('123', abs('123')); + + assertType('123', abs('-123')); + + assertType('123.0', abs('123.0')); + + assertType('123.0', abs('-123.0')); + + assertType('float|int<0, max>', abs('foo')); + } + + public function mixedUnion(mixed $value): void + { + /** @var 1.0|int<2, 3> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var -1.0|int<-3, -2> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var 2.0|int<1, 3> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -2.0|int<-3, -1> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -1.0|int<2, 3>|numeric-string $value */ + assertType('float|int<0, max>', abs($value)); + } + + public function intersection(mixed $value): void + { + /** @var int&int<-10, 10> $value */ + assertType('int<0, 10>', abs($value)); + } + + public function invalidType(mixed $nonInt): void + { + /** @var string $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var string|positive-int $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var 'foo' $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var array $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var non-empty-list $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var object $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var \DateTime $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var null $nonInt */ + assertType('0', abs($nonInt)); + + assertType('float|int<0, max>', abs('foo')); + + assertType('0', abs(null)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php b/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php new file mode 100644 index 0000000000..1010ae098b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php @@ -0,0 +1,32 @@ + */ + use Foo; + + public function doFoo() + { + return 1; + } + +} + +function (UseFoo $f): void { + assertType('int', $f->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php new file mode 100644 index 0000000000..d21d17e739 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php @@ -0,0 +1,30 @@ +getFileName()); + assertType('int', $r->getStartLine()); + assertType('int', $r->getEndLine()); + assertType('string|false', $r->getDocComment()); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant|false', $r->getReflectionConstant($s)); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass|false', $r->getParentClass()); + assertType('non-empty-string|false', $r->getExtensionName()); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType|null', $r->getBackingType()); +}; + +function (ReflectionEnumBackedCase $r): void { + assertType('string|false', $r->getDocComment()); + assertType(ReflectionType::class . '|null', $r->getType()); +}; + +function (ReflectionEnumUnitCase $r): void { + assertType('string|false', $r->getDocComment()); + assertType(ReflectionType::class . '|null', $r->getType()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php new file mode 100644 index 0000000000..46c43086e0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php @@ -0,0 +1,17 @@ += 8.1 + +namespace AllowedSubtypesEnum; + +use function PHPStan\Testing\assertType; + +enum Foo { + case A; + case B; + case C; +} + +function foo(Foo $foo): void { + assertType('AllowedSubtypesEnum\\Foo', $foo); + + if ($foo === Foo::B) { + return; + } + + assertType('AllowedSubtypesEnum\\Foo~AllowedSubtypesEnum\\Foo::B', $foo); + + if ($foo === Foo::C) { + return; + } + + assertType('AllowedSubtypesEnum\\Foo::A', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php new file mode 100644 index 0000000000..4336e79d64 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php @@ -0,0 +1,17 @@ + $arr1 + * @param array $arr2 + * @param array $arr3 + * @param array $arr4 + * @param array $arr5 + * @param array $arr6 + * @param array $arr7 + * @param array $arr8 + * @param array{foo: 1, bar?: 2} $arr9 + * @param array<'foo'|'bar', string> $arr10 + * @param list $list + * @param non-empty-array $nonEmpty + */ + public function sayHello( + array $arr1, + array $arr2, + array $arr3, + array $arr4, + array $arr5, + array $arr6, + array $arr7, + array $arr8, + array $arr9, + array $arr10, + array $list, + array $nonEmpty, + int $case + ): void { + assertType('array', array_change_key_case($arr1)); + assertType('array', array_change_key_case($arr1, CASE_LOWER)); + assertType('array', array_change_key_case($arr1, CASE_UPPER)); + assertType('array', array_change_key_case($arr1, $case)); + + assertType('array', array_change_key_case($arr2)); + assertType('array', array_change_key_case($arr2, CASE_LOWER)); + assertType('array', array_change_key_case($arr2, CASE_UPPER)); + assertType('array', array_change_key_case($arr2, $case)); + + assertType('array', array_change_key_case($arr3)); + assertType('array', array_change_key_case($arr3, CASE_LOWER)); + assertType('array', array_change_key_case($arr3, CASE_UPPER)); + assertType('array', array_change_key_case($arr3, $case)); + + assertType('array', array_change_key_case($arr4)); + assertType('array', array_change_key_case($arr4, CASE_LOWER)); + assertType('array', array_change_key_case($arr4, CASE_UPPER)); + assertType('array', array_change_key_case($arr4, $case)); + + assertType('array', array_change_key_case($arr5)); + assertType('array', array_change_key_case($arr5, CASE_LOWER)); + assertType('array', array_change_key_case($arr5, CASE_UPPER)); + assertType('array', array_change_key_case($arr5, $case)); + + assertType('array', array_change_key_case($arr6)); + assertType('array', array_change_key_case($arr6, CASE_LOWER)); + assertType('array', array_change_key_case($arr6, CASE_UPPER)); + assertType('array', array_change_key_case($arr6, $case)); + + assertType('array', array_change_key_case($arr7)); + assertType('array', array_change_key_case($arr7, CASE_LOWER)); + assertType('array', array_change_key_case($arr7, CASE_UPPER)); + assertType('array', array_change_key_case($arr7, $case)); + + assertType('array', array_change_key_case($arr8)); + assertType('array', array_change_key_case($arr8, CASE_LOWER)); + assertType('array', array_change_key_case($arr8, CASE_UPPER)); + assertType('array', array_change_key_case($arr8, $case)); + + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9)); + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9, CASE_LOWER)); + assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr9, CASE_UPPER)); + assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr9, $case)); + + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10)); + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10, CASE_LOWER)); + assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr10, CASE_UPPER)); + assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr10, $case)); + + assertType('list', array_change_key_case($list)); + assertType('list', array_change_key_case($list, CASE_LOWER)); + assertType('list', array_change_key_case($list, CASE_UPPER)); + assertType('list', array_change_key_case($list, $case)); + + assertType('non-empty-array', array_change_key_case($nonEmpty)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_LOWER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_UPPER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, $case)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php new file mode 100644 index 0000000000..5c36290a8f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php @@ -0,0 +1,20 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayChunkPhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param int<-5, -10> $negativeRange + * @param int<-5, 0> $negativeWithZero + */ + public function negativeLength(array $arr, $negativeRange, $negativeWithZero) { + assertType('*NEVER*', array_chunk($arr, $negativeRange)); + assertType('*NEVER*', array_chunk($arr, $negativeWithZero)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php new file mode 100644 index 0000000000..d65050061d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php @@ -0,0 +1,32 @@ += 8.1 + +declare(strict_types = 1); + +namespace ArrayChunkPhp81; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param 1|2 $union + */ + public function enumTest($union) { + $arr = []; + $arr[] = Status::DRAFT; + $arr[] = Status::PUBLISHED; + if (rand(0,1)) { + $arr[] = Status::ARCHIVED; + } + + assertType('array{array{ArrayChunkPhp81\Status::DRAFT, ArrayChunkPhp81\Status::PUBLISHED}, array{0?: ArrayChunkPhp81\Status::ARCHIVED}}|array{array{ArrayChunkPhp81\Status::DRAFT}, array{ArrayChunkPhp81\Status::PUBLISHED}, array{0?: ArrayChunkPhp81\Status::ARCHIVED}}', array_chunk($arr, $union)); + } + +} + +enum Status +{ + case DRAFT; + case PUBLISHED; + case ARCHIVED; +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php new file mode 100644 index 0000000000..cedb50ddb7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -0,0 +1,99 @@ +>', array_chunk($arr, 2)); + assertType('list>', array_chunk($arr, 2, true)); + + /** @var array $arr */ + assertType('list>', array_chunk($arr, 2)); + assertType('list>', array_chunk($arr, 2, true)); + + /** @var non-empty-array $arr */ + assertType('non-empty-list>', array_chunk($arr, 1)); + assertType('non-empty-list>', array_chunk($arr, 1, true)); + } + + + public function constantArrays(array $arr): void + { + /** @var array{a: 0, 17: 1, b: 2} $arr */ + assertType('array{array{0, 1}, array{2}}', array_chunk($arr, 2)); + assertType('array{array{a: 0, 17: 1}, array{b: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{0}, array{1}, array{2}}', array_chunk($arr, 1)); + assertType('array{array{a: 0}, array{17: 1}, array{b: 2}}', array_chunk($arr, 1, true)); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{array{a: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{a: 0, b?: 1, c: 2}}', array_chunk($arr, 3, true)); + assertType('array{array{a: 0}, array{b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 1, true)); + + /** @var array{a?: 0, b?: 1, c?: 2} $arr */ + assertType('array{array{a?: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + } + + /** + * @param int<2, 3> $positiveRange + * @param 2|3 $positiveUnion + */ + public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveRange)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveRange, true)); + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveUnion)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveUnion, true)); + } + + /** + * @param positive-int $positiveInt + * @param int<50, max> $bigger50 + */ + public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) { + assertType('list', array_chunk($arr, $positiveInt)); + assertType('list', array_chunk($arr, $bigger50)); + } + + /** + * @param int<1, 4> $oneToFour + * @param int<1, 5> $tooBig + */ + function testLimits(array $arr, int $oneToFour, int $tooBig) { + /** @var array{a: 0, b?: 1, c: 2, d: 3} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2|3, 3?: 3}, 1?: array{0?: 2|3, 1?: 3}}|array{array{0}, array{0?: 1|2, 1?: 2}, array{0?: 2|3, 1?: 3}, array{0?: 3}}', array_chunk($arr, $oneToFour)); + assertType('non-empty-list>', array_chunk($arr, $tooBig)); + } + + /** @param array $map */ + public function offsets(array $arr, array $map): void + { + if (array_key_exists('foo', $arr)) { + assertType('non-empty-list', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2, true)); + } + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + assertType('non-empty-list', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2, true)); + } + + if (array_key_exists('foo', $map)) { + assertType('non-empty-list>', array_chunk($map, 2)); + assertType('non-empty-list>', array_chunk($map, 2, true)); + } + if (array_key_exists('foo', $map) && $map['foo'] === 'bar') { + assertType('non-empty-list>', array_chunk($map, 2)); + assertType('non-empty-list>', array_chunk($map, 2, true)); + } + } + +} diff --git a/tests/PHPStan/Analyser/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..e55e7a38ba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -0,0 +1,239 @@ += 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('list', array_column($array, 'column', null)); + 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')); + assertType('list', array_column($array, 'column', null)); + } + + /** @param array{} $array */ + public function testArray3(array $array): void + { + assertType('array{}', array_column($array, 'column')); + assertType('array{}', array_column($array, 'column', null)); + 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('list', array_column($array, 'column', null)); + 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', null)); + 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{string}", array_column($array, 'column', null)); + 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("list<'foo'>", array_column($array, 'column', null)); + 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')); + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2', null)); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-list', array_column($array, 'column')); + assertType('non-empty-list', array_column($array, 'column', null)); + 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{0?: 'foo'}", array_column($array, 'column', null)); + 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("list<'foo'>", array_column($array, 'column', null)); + 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('list', array_column($array, 'column', null)); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('list', array_column($array, 'nodeName')); + assertType('list', array_column($array, 'nodeName', null)); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo', null)); + 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-list', array_column($array, 'nodeName', null)); + 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('list', array_column($array, 'foo', null)); + 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('array{string}', array_column($array, 'nodeName', null)); + 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('list', array_column($array, 'foo', null)); + 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', null)); + 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..4f830b96d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -0,0 +1,254 @@ +> $array */ + public function testArray1(array $array): void + { + assertType('list', array_column($array, 'column')); + assertType('list', array_column($array, 'column', null)); + 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')); + assertType('list', array_column($array, 'column', null)); + } + + /** @param array{} $array */ + public function testArray3(array $array): void + { + assertType('array{}', array_column($array, 'column')); + assertType('array{}', array_column($array, 'column', null)); + 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('list', array_column($array, 'column', null)); + 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', null)); + 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{string}", array_column($array, 'column', null)); + 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("list<'foo'>", array_column($array, 'column', null)); + 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')); + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2', null)); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-list', array_column($array, 'column')); + assertType('non-empty-list', array_column($array, 'column', null)); + 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{0?: 'foo'}", array_column($array, 'column', null)); + 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{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null)); + 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{0: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null)); + 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("list<'foo'>", array_column($array, 'column', null)); + 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('list', array_column($array, 'column', null)); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('list', array_column($array, 'nodeName')); + assertType('list', array_column($array, 'nodeName', null)); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo', null)); + 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-list', array_column($array, 'nodeName', null)); + 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('list', array_column($array, 'foo', null)); + 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('array{string}', array_column($array, 'nodeName', null)); + 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('list', array_column($array, 'foo', null)); + 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..7982233b61 --- /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..0f415089bd --- /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..9a56ef0cfb --- /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-php7.php b/tests/PHPStan/Analyser/nsrt/array-filter-php7.php new file mode 100644 index 0000000000..e0197e9f5b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-php7.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-filter-php8.php b/tests/PHPStan/Analyser/nsrt/array-filter-php8.php new file mode 100644 index 0000000000..2461fa9c69 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-php8.php @@ -0,0 +1,42 @@ += 8.0 + +namespace ArrayFilterPHP8; + +use function PHPStan\Testing\assertType; + +function withoutAnyArgs(): void +{ + $filtered1 = array_filter(); + assertType('array', $filtered1); +} + +/** + * @param mixed $var1 + */ +function withMixedInsteadOfArray($var1): void +{ + $filtered1 = array_filter($var1); + assertType('array', $filtered1); +} + +/** + * @param array $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-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-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..d4e1c621d4 --- /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('non-empty-array', $foo); +} + +$baz = []; + +if (array_is_list($baz)) { + assertType('array{}', $baz); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php b/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php new file mode 100644 index 0000000000..b14b8c97df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php @@ -0,0 +1,26 @@ += 8.0 + +namespace ArrayKeyExistsExtension; + +use function array_key_exists; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param array $a + * @return void + */ + public function doFoo(array $a, string $key, int $anotherKey): void + { + assertType('false', array_key_exists(2, $a)); + assertType('bool', array_key_exists('foo', $a)); + assertType('false', array_key_exists('2', $a)); + + $a = ['foo' => 2, 3 => 'bar']; + assertType('true', array_key_exists('foo', $a)); + assertType('true', array_key_exists(3, $a)); + assertType('true', array_key_exists('3', $a)); + assertType('false', array_key_exists(4, $a)); + + if (array_key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (array_key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + + $empty = []; + assertType('false', array_key_exists('foo', $empty)); + assertType('false', array_key_exists($key, $empty)); + } + + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (array_key_exists($key1, $a)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $a)) { + assertType('lowercase-string&numeric-string&uppercase-string', $key2); + } + if (array_key_exists($key3, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); + } + if (array_key_exists($key4, $a)) { + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); + } + if (array_key_exists($key5, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); + } + + if (array_key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (array_key_exists($key2, $b)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $b)) { + assertType('string', $key3); + } + if (array_key_exists($key4, $b)) { + assertType('string', $key4); + } + if (array_key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (array_key_exists($key1, $c)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $c)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (array_key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (array_key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (array_key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (array_key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (array_key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (array_key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (array_key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array-key.php b/tests/PHPStan/Analyser/nsrt/array-key.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-key.php rename to tests/PHPStan/Analyser/nsrt/array-key.php diff --git a/tests/PHPStan/Analyser/data/array-map-closure.php b/tests/PHPStan/Analyser/nsrt/array-map-closure.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-map-closure.php rename to tests/PHPStan/Analyser/nsrt/array-map-closure.php diff --git a/tests/PHPStan/Analyser/nsrt/array-map.php b/tests/PHPStan/Analyser/nsrt/array-map.php new file mode 100644 index 0000000000..5dbafb1390 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-map.php @@ -0,0 +1,119 @@ + $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); +} + +class Foo +{ + /** + * @template T of int + * @param T $n + * @return (T is 3 ? 'Fizz' : (T is 5 ? 'Buzz' : T)) + */ + public static function fizzbuzz(int $n): int|string + { + return match ($n) { + 3 => 'Fizz', + 5 => 'Buzz', + default => $n, + }; + } + + public function doFoo(): void + { + $a = range(0, 1); + + assertType("array{'0', '1'}", array_map('strval', $a)); + assertType("array{'0', '1'}", array_map(strval(...), $a)); + assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => strval($v), $a)); + assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => (string)$v, $a)); + } + + public function doFizzBuzz(): void + { + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([__CLASS__, 'fizzbuzz'], range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([$this, 'fizzbuzz'], range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map(self::fizzbuzz(...), range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map($this->fizzbuzz(...), range(1, 6))); + } + + /** + * @param array $array + */ + public function doUppercase(array $array): void + { + assertType("array", array_map(strtoupper(...), $array)); + assertType("array{'A', 'B'}", array_map(strtoupper(...), ['A', 'B'])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-merge.php b/tests/PHPStan/Analyser/nsrt/array-merge.php new file mode 100644 index 0000000000..6dd7a13005 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-merge.php @@ -0,0 +1,54 @@ + 17, 'a', 'bar' => 18, 'b']; + $bar = [99 => 'b', 'bar' => 19, 98 => 'c']; + $baz = array_merge($foo, $bar); + + assertType('array{foo: 17, 0: \'a\', bar: 19, 1: \'b\', 2: \'b\', 3: \'c\'}', $baz); +} + +/** + * @param string[][] $foo + * @param array> $bar1 + * @param non-empty-array> $bar2 + * @param non-empty-array> $bar3 + */ +function unpackingArrays(array $foo, array $bar1, array $bar2, array $bar3): void +{ + assertType('array', array_merge([], ...$foo)); + assertType('array', array_merge([], ...$bar1)); + assertType('non-empty-array', array_merge([], ...$bar2)); + assertType('array', array_merge([], ...$bar3)); +} + +function unpackingConstantArrays(): void +{ + assertType('array{}', array_merge([], ...[])); + assertType('array{17}', array_merge([], [17])); + assertType('array{17}', array_merge([], ...[[17]])); + assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ['bar' => 'baz2', 17])); + assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ...[['bar' => 'baz2', 17]])); +} + +/** + * @param list $a + * @param list $b + * @return void + */ +function listIsStillList(array $a, array $b): void +{ + assertType('list', array_merge($a, $b)); + + $c = []; + foreach ($a as $v) { + $c = array_merge($a, $c); + } + assertType('list', $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php new file mode 100644 index 0000000000..f0d86e61b6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-merge2.php @@ -0,0 +1,62 @@ + 3])); + assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge(rand(0, 1) ? $array1 : $array2, [])); + assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge([], ...[rand(0, 1) ? $array1 : $array2])); + assertType("array{foo: 1, bar: 2, 0: 2, 1: 3}", array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])); + } + + /** + * @param int[] $array1 + * @param string[] $array2 + */ + public function arrayMergeSimple($array1, $array2): void + { + assertType("array", array_merge($array1, $array1)); + assertType("array", array_merge($array1, $array2)); + assertType("array", array_merge($array2, $array1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayMergeUnionType($array1, $array2): void + { + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayMergeUnionTypeArrayShapes($array1, $array2): void + { + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); + } +} diff --git a/tests/PHPStan/Analyser/data/array-next.php b/tests/PHPStan/Analyser/nsrt/array-next.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-next.php rename to tests/PHPStan/Analyser/nsrt/array-next.php diff --git a/tests/PHPStan/Analyser/nsrt/array-offset-unset.php b/tests/PHPStan/Analyser/nsrt/array-offset-unset.php new file mode 100644 index 0000000000..0c9395d6ce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-offset-unset.php @@ -0,0 +1,16 @@ + $list + */ +function foo(array $list) { + assertType('array<0|1, mixed>', $list); + unset($list[0]); + assertType('array<1, mixed>', $list); + unset($list[1]); + assertType('array{}', $list); +} diff --git a/tests/PHPStan/Analyser/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..93990c4f5b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-replace.php @@ -0,0 +1,105 @@ +", 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)); + } + + /** + * @param array{foo: '1', bar: '2'} $array1 + * @param array $array2 + * @param array $array3 + */ + public function arrayReplaceArrayShapeAndGeneralArray($array1, $array2, $array3): void + { + assertType("non-empty-array", array_replace($array1, $array2)); + assertType("non-empty-array", array_replace($array2, $array1)); + + assertType("non-empty-array<'bar'|'foo'|int, string>", array_replace($array1, $array3)); + assertType("non-empty-array<'bar'|'foo'|int, string>", array_replace($array3, $array1)); + + assertType("array", array_replace($array2, $array3)); + } + + /** + * @param array{0: 1, 1: 2} $array1 + * @param array{1: 3, 2: 4} $array2 + */ + public function arrayReplaceNumericKeys($array1, $array2): void + { + assertType("array{1, 3, 4}", array_replace($array1, $array2)); + } + + /** + * @param list $array1 + * @param list $array2 + */ + public function arrayReplaceLists($array1, $array2): void + { + assertType("list", array_replace($array1, $array2)); + } +} 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('int', array_search('foo', $arr, true)); + assertType('int', 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('1', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + assertType('0|1|2|false', array_search($string, $arr, false)); + + /** @var array{} $arr */ + assertType('false', array_search('b', $arr, true)); + assertType('false', array_search('b', $arr)); + assertType('false', array_search($string, $arr, true)); + assertType('false', array_search($string, $arr, false)); + + /** @var array{1, '1', '2'} $arr */ + assertType('1', array_search('1', $arr, true)); + assertType('0|1', array_search('1', $arr)); + assertType('1|2|false', array_search($string, $arr, true)); + assertType('0|1|2|false', array_search($string, $arr, false)); + } + + 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('1|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('1|2', 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/data/array-typehint-without-null-in-phpdoc.php b/tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-typehint-without-null-in-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php diff --git a/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php new file mode 100644 index 0000000000..a03dbb75c9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php @@ -0,0 +1,68 @@ += 8.1 + +namespace ArrayUnpackingWithStringKeys; + +use function PHPStan\Testing\assertType; + +$foo = ['a' => 0, ...['a' => 1], ...['b' => 2]]; + +assertType('array{a: 1, b: 2}', $foo); + +$bar = [1, ...['a' => 1], ...['b' => 2]]; + +assertType('array{0: 1, a: 1, b: 2}', $bar); + +/** + * @param array $a + * @param array $b + */ +function foo(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function bar(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function baz(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('array', $c); +} + +/** + * @param non-empty-array $a + * @param array $b + */ +function nonEmptyArray1(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} + +/** + * @param array $a + * @param non-empty-array $b + */ +function nonEmptyArray2(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-unshift.php b/tests/PHPStan/Analyser/nsrt/array-unshift.php new file mode 100644 index 0000000000..933aad522d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-unshift.php @@ -0,0 +1,74 @@ + $c + * @param array $d + * @param list $e + */ +function arrayUnshift(array $a, array $b, array $c, array $d, array $e, array $arr): void +{ + array_unshift($a, ...$b); + assertType('array', $a); + + /** @var non-empty-array $arr */ + array_unshift($arr, ...$b); + assertType('non-empty-array', $arr); + + array_unshift($b, ...[]); + assertType('array', $b); + + array_unshift($c, ...[19, 'baz', false]); + assertType('non-empty-array<\'baz\'|int|false>', $c); + + /** @var array $d1 */ + $d1 = []; + array_unshift($d, ...$d1); + assertType('array', $d); + + /** @var list $e1 */ + $e1 = []; + array_unshift($e, ...$e1); + assertType('list', $e); +} + +function arrayUnshiftConstantArray(): void +{ + $a = ['foo' => 17, 'a', 'bar' => 18,]; + array_unshift($a, ...[19, 'baz', false]); + assertType('array{0: 19, 1: \'baz\', 2: false, foo: 17, 3: \'a\', bar: 18}', $a); + + $b = ['foo' => 17, 'a', 'bar' => 18]; + array_unshift($b, 19, 'baz', false); + assertType('array{0: 19, 1: \'baz\', 2: false, foo: 17, 3: \'a\', bar: 18}', $b); + + $c = ['foo' => 17, 'a', 'bar' => 18]; + array_unshift($c, ...[]); + assertType('array{foo: 17, 0: \'a\', bar: 18}', $c); + + $d = []; + array_unshift($d, ...[]); + assertType('array{}', $d); + + $e = []; + array_unshift($e, 19, 'baz', false); + assertType('array{19, \'baz\', false}', $e); + + $f = [17]; + /** @var array $f1 */ + $f1 = []; + array_unshift($f, ...$f1); + assertType('non-empty-list<17|bool|null>', $f); + + $g = [new stdClass()]; + array_unshift($g, ...[new stdClass(), new stdClass()]); + assertType('array{stdClass, stdClass, stdClass}', $g); +} diff --git a/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php new file mode 100644 index 0000000000..5dc721664e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php @@ -0,0 +1,127 @@ +', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_keys($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_keys.php b/tests/PHPStan/Analyser/nsrt/array_keys.php new file mode 100644 index 0000000000..6808bf36b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_keys.php @@ -0,0 +1,27 @@ += 8.0 + +namespace ArrayKeys; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello($mixed): void + { + if(is_array($mixed)) { + assertType('list<(int|string)>', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_keys($mixed)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php new file mode 100644 index 0000000000..2918ebeb89 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -0,0 +1,40 @@ + $i], ['bar' => $s]); + assertType('non-empty-list', $result); + } + + /** + * @param non-empty-array $array + * @param non-empty-array $other + */ + public function arrayMapNull(array $array, array $other): void + { + assertType('array{}', array_map(null, [])); + assertType('array{foo: true}', array_map(null, ['foo' => true])); + assertType('non-empty-list', array_map(null, [1, 2, 3], [4, 5, 6])); + + assertType('non-empty-array', array_map(null, $array)); + assertType('non-empty-list', array_map(null, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $other)); + + assertType('array{1}|array{true}', array_map(null, rand() ? [1] : [true])); + assertType('array{1}|array{true, false}', array_map(null, rand() ? [1] : [true, false])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_pad.php b/tests/PHPStan/Analyser/nsrt/array_pad.php new file mode 100644 index 0000000000..5b28a64458 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_pad.php @@ -0,0 +1,78 @@ + $arrayString + * @param array $arrayInt + * @param non-empty-array $nonEmptyArrayString + * @param non-empty-array $nonEmptyArrayInt + * @param list $listString + * @param list $listInt + * @param non-empty-list $nonEmptyListString + * @param non-empty-list $nonEmptyListInt + * @param int $int + * @param positive-int $positiveInt + * @param negative-int $negativeInt + * @param positive-int|negative-int $nonZero + */ + public function test( + $arrayString, + $arrayInt, + $nonEmptyArrayString, + $nonEmptyArrayInt, + $listString, + $listInt, + $nonEmptyListString, + $nonEmptyListInt, + $int, + $positiveInt, + $negativeInt, + $nonZero, + ): void + { + assertType('array', array_pad($arrayString, $int, 'foo')); + assertType('non-empty-array', array_pad($arrayString, $positiveInt, 'foo')); + assertType('non-empty-array', array_pad($arrayString, $negativeInt, 'foo')); + assertType('non-empty-array', array_pad($arrayString, $nonZero, 'foo')); + + assertType('array', array_pad($arrayInt, $int, 'foo')); + assertType('non-empty-array', array_pad($arrayInt, $positiveInt, 'foo')); + assertType('non-empty-array', array_pad($arrayInt, $negativeInt, 'foo')); + assertType('non-empty-array', array_pad($arrayInt, $nonZero, 'foo')); + + assertType('non-empty-array', array_pad($nonEmptyArrayString, $int, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayString, $positiveInt, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayString, $negativeInt, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayString, $nonZero, 'foo')); + + assertType('non-empty-array', array_pad($nonEmptyArrayInt, $int, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayInt, $positiveInt, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayInt, $negativeInt, 'foo')); + assertType('non-empty-array', array_pad($nonEmptyArrayInt, $nonZero, 'foo')); + + assertType('list', array_pad($listString, $int, 'foo')); + assertType('non-empty-list', array_pad($listString, $positiveInt, 'foo')); + assertType('non-empty-list', array_pad($listString, $negativeInt, 'foo')); + assertType('non-empty-list', array_pad($listString, $nonZero, 'foo')); + + assertType('list<\'foo\'|int>', array_pad($listInt, $int, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($listInt, $positiveInt, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($listInt, $negativeInt, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($listInt, $nonZero, 'foo')); + + assertType('non-empty-list', array_pad($nonEmptyListString, $int, 'foo')); + assertType('non-empty-list', array_pad($nonEmptyListString, $positiveInt, 'foo')); + assertType('non-empty-list', array_pad($nonEmptyListString, $negativeInt, 'foo')); + assertType('non-empty-list', array_pad($nonEmptyListString, $nonZero, 'foo')); + + assertType('non-empty-list<\'foo\'|int>', array_pad($nonEmptyListInt, $int, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($nonEmptyListInt, $positiveInt, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($nonEmptyListInt, $negativeInt, 'foo')); + assertType('non-empty-list<\'foo\'|int>', array_pad($nonEmptyListInt, $nonZero, 'foo')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php new file mode 100644 index 0000000000..92385d0786 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -0,0 +1,386 @@ + $arr + * @return void + */ +function insertViaArraySplice(array $arr): void +{ + $brr = $arr; + $extract = array_splice($brr, 0, 0, 1); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, [1]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, ''); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, ['']); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, null); + assertType('array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, [null]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, new Foo()); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, [new \stdClass()]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, false); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, [false]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0); + assertType('array{}', $brr); + assertType('list', $extract); +} + +function constantArrays(array $arr, array $arr2): void +{ + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 1, 2, ['hello']); + assertType('array{\'foo\', \'hello\'}', $arr); + assertType('array{b: \'bar\', 0: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{\'hello\', \'baz\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -2, ['hello']); + assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -1, -1, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -2, -2, ['hello']); + assertType('array{0: \'foo\', 1: \'hello\', b: \'bar\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 99, 0, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 1, 99, ['hello']); + assertType('array{\'foo\', \'hello\'}', $arr); + assertType('array{b: \'bar\', 0: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -99, 99, ['hello']); + assertType('array{\'hello\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -99, ['hello']); + assertType('array{0: \'hello\', 1: \'foo\', b: \'bar\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -2, 1, ['hello']); + assertType('array{\'foo\', \'hello\', \'baz\'}', $arr); + assertType('array{b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -1, 1, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\'}', $arr); + assertType('array{\'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, null, ['hello']); + assertType('array{\'hello\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0); + assertType('array{}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array<\stdClass> $arr2 */ + $arr; + $extract = array_splice($arr, 1, 1, $arr2); + assertType('non-empty-array, \'baz\'|\'foo\'|stdClass>', $arr); + assertType('array{b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array<\stdClass> $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|stdClass>', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int} $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('array{0: \'x\'|\'z\', 1: \'y\'|int, 2: \'baz\'|int, b: \'bar\', 3?: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int}|array $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|\'x\'|\'y\'|\'z\'|int|object|null>', $arr); + assertType('array{\'foo\'}', $extract); +} + +function constantArraysWithOptionalKeys(array $arr): void +{ + /** + * @see https://3v4l.org/2UJ3u + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + assertType('array{a?: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/Aq4l6 + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a?: 0, b?: 1, 0: \'hello\', c?: 2}', $arr); + assertType('array{b?: 1, c?: 2}', $extract); + + /** + * @see https://3v4l.org/GBMps + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a?: 0, b: 1, 0: \'hello\', c: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/dQVgY + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c: 2}', $arr); + assertType('array{a?: 0, b: 1}', $extract); + + /** + * @see https://3v4l.org/5XWRC + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + assertType('array{a: 0}', $extract); + + /** + * @see https://3v4l.org/QXZre + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + assertType('array{b?: 1, c?: 2}', $extract); + + /** + * @see https://3v4l.org/4JvMu + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a: 0, b?: 1, 0: \'hello\', c: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/srHon + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c: 2}', $arr); + assertType('array{a: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/d0b0c + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: 1, c?: 2}', $arr); + assertType('array{a: 0}', $extract); + + /** + * @see https://3v4l.org/OPfIf + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + assertType('array{b: 1}', $extract); + + /** + * @see https://3v4l.org/b9R9E + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a: 0, b: 1, 0: \'hello\', c?: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/0lFX6 + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c?: 2}', $arr); + assertType('array{a: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/PLHYv + * @var array{a: 0, b?: 1, c?: 2, d: 3} $arr + */ + $arr; + $extract = array_splice($arr, 1, 2, ['hello']); + assertType('array{a: 0, 0: \'hello\', d?: 3}', $arr); + assertType('array{b?: 1, c?: 2, d?: 3}', $extract); + + /** + * @see https://3v4l.org/Li5bj + * @var array{a: 0, b?: 1, c?: 2, d: 3} $arr + */ + $arr; + $extract = array_splice($arr, -2, 2, ['hello']); + assertType('array{a?: 0, b?: 1, 0: \'hello\'}', $arr); + assertType('array{a?: 0, b?: 1, c?: 2, d: 3}', $extract); +} + +function offsets(array $arr): void +{ + if (array_key_exists(1, $arr)) { + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + assertType('array', $extract); + } + + if (array_key_exists(1, $arr)) { + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffset(1)', $arr); + assertType('array{}', $extract); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + assertType('array', $extract); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr); + assertType('array{}', $extract); + } +} + +function lists(array $arr): void +{ + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-list', $arr); + assertType('list', $extract); + + /** @var non-empty-list $arr */ + $arr; + $extract = array_splice($arr, 0, 1); + assertType('list', $arr); + assertType('non-empty-list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-list', $arr); + assertType('array{}', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, null, 'hello'); + assertType('non-empty-list', $arr); + assertType('list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, null); + assertType('array{}', $arr); + assertType('list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 1); + assertType('list', $arr); + assertType('list', $extract); +} diff --git a/tests/PHPStan/Analyser/nsrt/array_values-php7.php b/tests/PHPStan/Analyser/nsrt/array_values-php7.php new file mode 100644 index 0000000000..db378c95aa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_values-php7.php @@ -0,0 +1,20 @@ +', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_values($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_values.php b/tests/PHPStan/Analyser/nsrt/array_values.php new file mode 100644 index 0000000000..18074963a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_values.php @@ -0,0 +1,48 @@ += 8.0 + +namespace ArrayValues; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('list', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_values($mixed)); + } + } + + /** + * @param list $list + */ + public function foo2($list): void + { + if(is_array($list)) { + assertType('list', array_values($list)); + } else { + assertType('*NEVER*', $list); + assertType('*NEVER*', array_values($list)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers)); + } + + /** + * @param array> $a + */ + public function arrayMap(array $a): void + { + assertType('array>', array_map(array_values(...), $a)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php new file mode 100644 index 0000000000..a508035d19 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php @@ -0,0 +1,28 @@ + assertType('int', $context))($integer); + + (fn($context) => assertType('array{a: int}', $context))($array); + + (fn($context) => assertType('string|null', $context))($nullableString); + + (fn($a, $b, $c) => assertType('array{int, array{a: int}, string|null}', [$a, $b, $c]))($integer, $array, $nullableString); + + (fn($a, $b, $c = null) => assertType('array{int, array{a: int}, mixed}', [$a, $b, $c]))($integer, $array); + + ($callback = fn($context) => assertType('int', $context))($integer); + } + +} diff --git a/tests/PHPStan/Analyser/data/arrow-function-return-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/arrow-function-return-type.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php diff --git a/tests/PHPStan/Analyser/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('non-empty-array', $arr); + } +} +/** + * @param mixed[] $arr + */ +function takesArrayIfTrue1(array $arr) : void { + assertType('array', $arr); + + if (!validateStringArrayIfTrue($arr)) { + assertType('non-empty-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('non-empty-array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfFalse1(array $arr) : void { + assertType('array', $arr); + + if (validateStringArrayIfFalse($arr)) { + assertType('non-empty-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..2ead45175f --- /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('false', $a < $b); + assertType('false', $a <= $b); + assertType('true', $a > $b); + assertType('true', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('false', $a && $b); + assertType('bool', $a || $b); + assertType('false', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsArray(Number $a, array $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsObject(Number $a, object $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + /** @param resource $b */ + public function bcVsResource(Number $a, $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsCallable(Number $a, callable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsIterable(Number $a, iterable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsStringable(Number $a, \Stringable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsNever(Number $a): void + { + for ($b = 1; $b < count([]); $b++) { + assertType('*NEVER*', $a + $b); + assertType('*ERROR*', $a - $b); // Inconsistency: getPlusType handles never types right at the beginning, getMinusType doesn't. + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*NEVER*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*NEVER*', $a << $b); + assertType('*NEVER*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('*NEVER*', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*NEVER*', $a & $b); + assertType('*NEVER*', $a ^ $b); + assertType('*NEVER*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php b/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php new file mode 100644 index 0000000000..6b273c9672 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php @@ -0,0 +1,44 @@ +getBenevolent(); + assertType('array|null', $dbresponse); + + if ($dbresponse === null) {return;} + + assertType('array', $dbresponse); + assertType('(float|int|string|null)', $dbresponse['Value']); + assertType('int<0, max>', strlen($dbresponse['Value'])); + } + + /** + * @return array>|null + */ + private function getBenevolent(): ?array{ + return rand(10) > 1 ? null : []; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bitwise-not.php b/tests/PHPStan/Analyser/nsrt/bitwise-not.php new file mode 100644 index 0000000000..37c29f8f97 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bitwise-not.php @@ -0,0 +1,21 @@ +format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($monthDay1 === $month){ + return true; + } + + assertType('bool', $monthDay2 === $month); + + return $monthDay2 === $month; + } + + public function bar1(int $month): bool + { + $day1 = new \DateTime('2022-01-01'); + $day2 = new \DateTime('2022-05-01'); + + $monthDay1 = (int) $day1->format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($month === $monthDay1){ + return true; + } + + assertType('bool', $month === $monthDay2); + + return $monthDay2 === $month; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10037.php b/tests/PHPStan/Analyser/nsrt/bug-10037.php new file mode 100644 index 0000000000..56c49c331b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10037.php @@ -0,0 +1,98 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10037; + +interface Identifier +{} + +interface Document +{} + +/** @template T of Identifier */ +interface Fetcher +{ + /** @phpstan-assert-if-true T $identifier */ + public function supports(Identifier $identifier): bool; + + /** @param T $identifier */ + public function fetch(Identifier $identifier): Document; +} + +/** @implements Fetcher */ +final readonly class PostFetcher implements Fetcher +{ + public function supports(Identifier $identifier): bool + { + return $identifier instanceof PostIdentifier; + } + + public function fetch(Identifier $identifier): Document + { + // SA knows $identifier is instance of PostIdentifier here + return $identifier->foo(); + } +} + +class PostIdentifier implements Identifier +{ + public function foo(): Document + { + return new class implements Document{}; + } +} + +function (Identifier $i): void { + $fetcher = new PostFetcher(); + \PHPStan\Testing\assertType('Bug10037\Identifier', $i); + if ($fetcher->supports($i)) { + \PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i); + $fetcher->fetch($i); + } else { + $fetcher->fetch($i); + } +}; + +class Post +{ +} + +/** @template T */ +abstract class Voter +{ + + /** @phpstan-assert-if-true T $subject */ + abstract function supports(string $attribute, mixed $subject): bool; + + /** @param T $subject */ + abstract function voteOnAttribute(string $attribute, mixed $subject): bool; + +} + +/** @extends Voter */ +class PostVoter extends Voter +{ + + /** @phpstan-assert-if-true Post $subject */ + function supports(string $attribute, mixed $subject): bool + { + + } + + function voteOnAttribute(string $attribute, mixed $subject): bool + { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + } +} + +function ($subject): void { + $voter = new PostVoter(); + \PHPStan\Testing\assertType('mixed', $subject); + if ($voter->supports('aaa', $subject)) { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + $voter->voteOnAttribute('aaa', $subject); + } else { + $voter->voteOnAttribute('aaa', $subject); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10071.php b/tests/PHPStan/Analyser/nsrt/bug-10071.php new file mode 100644 index 0000000000..ef25a1d61d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10071.php @@ -0,0 +1,24 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug10071; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public ?bool $bar = null; +} + + +function okIfBar(?Foo $foo = null): void +{ + if ($foo?->bar !== false) { + assertType(Foo::class . '|null', $foo); + } else { + assertType(Foo::class, $foo); + } + + assertType(Foo::class . '|null', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10080.php b/tests/PHPStan/Analyser/nsrt/bug-10080.php new file mode 100644 index 0000000000..1875d50dfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10080.php @@ -0,0 +1,76 @@ + assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $foo), + default => assertType('int', $foo), + }; +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10088.php b/tests/PHPStan/Analyser/nsrt/bug-10088.php new file mode 100644 index 0000000000..df9bd2b6c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10088.php @@ -0,0 +1,135 @@ +assertInstanceOfStdClass($date ?? null); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + /** + * @param mixed $m + * @phpstan-assert stdClass $m + */ + private function assertInstanceOfStdClass($m): void + { + if (!$m instanceof stdClass) { + throw new \Exception(); + } + } + + /** + * @param mixed[] $period + */ + public function testCarbon2(array $period): void + { + foreach ($period as $date) { + break; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $date); + assert(($date ?? null) instanceof stdClass); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + function constantIfElse(int $x): void { + $link_mode = $x > 10 ? "remove" : "add"; + + assertType('int', $x); + if ($link_mode === "add") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function constantIfElseShort(int $x): void { + $link_mode = $x > 10 ?: "remove"; + + assertType('int', $x); + if ($link_mode === "remove") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function nonEmptyArray(array $arr): void { + $link_mode = $arr ? "truethy-arr" : "falsey-arr"; + assertType('array', $arr); + if ($link_mode === "truethy-arr") { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param array $arr + * @param 0|positive-int $intRange + */ + function nonEmptyArrayViaInt(array $arr, $intRange): void { + $link_mode = $arr ? $intRange : -10; + assertType('array', $arr); + if ($link_mode >= 0) { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param string[] $arr + * @param 0|positive-int $posInt + */ + function overlappingIfElseType($arr, int $x, int $posInt): void { + $link_mode = $arr ? $posInt : $x; + assert($link_mode >= 0); + + assertType('array', $arr); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10092.php b/tests/PHPStan/Analyser/nsrt/bug-10092.php new file mode 100644 index 0000000000..aa5bacc049 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10092.php @@ -0,0 +1,43 @@ + */ +function int() { } + +/** @return TypeInterface<0> */ +function zero() { } + +/** @return TypeInterface> */ +function positive_int() { } + +/** @return TypeInterface */ +function numeric_string() { } + + +/** + * @template T + * + * @param TypeInterface $first + * @param TypeInterface $second + * @param TypeInterface ...$rest + * + * @return TypeInterface + */ +function union( + TypeInterface $first, + TypeInterface $second, + TypeInterface ...$rest +) { + +} + +function (): void { + assertType('Bug10092\TypeInterface', union(int(), numeric_string())); + assertType('Bug10092\TypeInterface>', union(positive_int(), zero())); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10122.php b/tests/PHPStan/Analyser/nsrt/bug-10122.php new file mode 100644 index 0000000000..b945f83075 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10122.php @@ -0,0 +1,54 @@ += 8.0 + +namespace Bug10131; + +use function PHPStan\Testing\assertType; + +class A { +} + +class B { + public A|null $a = null; +} + +/** + * @phpstan-return array{0:A|null, 1:B|null} + */ +function foo(A|null $a, B|null $b): array +{ + $a ??= $b?->a ?? throw new \Exception(); + + assertType(A::class, $a); + assertType(B::class . '|null', $b); + + return [$a, $b]; +} diff --git a/tests/PHPStan/Analyser/data/bug-1014.php b/tests/PHPStan/Analyser/nsrt/bug-1014.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-1014.php rename to tests/PHPStan/Analyser/nsrt/bug-1014.php index 9d0f0567b0..d146c3341f 100644 --- a/tests/PHPStan/Analyser/data/bug-1014.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1014.php @@ -1,5 +1,7 @@ ", $files); + + return empty($files) ? [] : [1,2]; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10201.php b/tests/PHPStan/Analyser/nsrt/bug-10201.php new file mode 100644 index 0000000000..a5cae6e11e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10201.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug10201; + +use function PHPStan\Testing\assertType; + +enum Hello +{ + case Hi; +} + +function bla(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true) && !($hello instanceof Hello)) { + assertType('string', $hello); + } +} + +function bla2(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true)) { + assertType('string', $hello); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1021.php b/tests/PHPStan/Analyser/nsrt/bug-1021.php new file mode 100644 index 0000000000..37e2f7244f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1021.php @@ -0,0 +1,32 @@ +', $x); + + if ($x) { + } +} + +function foo(array $x) { + if ($x) { + array_shift($x); + + assertType('array', $x); + + if ($x) { + echo ""; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10224.php b/tests/PHPStan/Analyser/nsrt/bug-10224.php new file mode 100644 index 0000000000..3158734620 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10224.php @@ -0,0 +1,33 @@ += 8.0 + +namespace Bug10254; + +use Closure; +use RuntimeException; +use function PHPStan\Testing\assertType; + +/** + * @template T + */ +class Option { + /** + * @param T $value + */ + private function __construct(private mixed $value) + { + } + + /** + * @template Tv + * @param Tv $value + * @return self + */ + public static function some($value): self + { + return new self($value); + } + + /** + * @template Tu + * + * @param (Closure(T): Tu) $closure + * + * @return Option + */ + public function map(Closure $closure): self + { + return new self($closure($this->unwrap())); + } + + /** + * @return T + */ + public function unwrap() + { + if ($this->value === null) { + throw new RuntimeException(); + } + + return $this->value; + } + + /** + * @template To + * @param self $other + * @return self + */ + public function zip(self $other) + { + return new self([ + $this->unwrap(), + $other->unwrap() + ]); + } +} + + +function (): void { + $value = Option::some(1) + ->zip(Option::some(2)); + + assertType('Bug10254\\Option', $value); + + $value1 = $value->map(function ($value) { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value1); + + $value2 = $value->map(function ($value): int { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value2); + +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10264.php b/tests/PHPStan/Analyser/nsrt/bug-10264.php new file mode 100644 index 0000000000..20b1361a25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10264.php @@ -0,0 +1,80 @@ + $list */ + $list = []; + + assertType('list', $list); + + assert((count($list) <= 1) === true); + assertType('list', $list); + } + + function doFoo2() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_NORMAL) <= 1) === true); + assertType('list', $list); + } + + /** @param list $c */ + public function sayHello(array $c): void + { + assertType('list', $c); + if (count($c) > 0) { + $c = array_map(fn() => new stdClass(), $c); + assertType('non-empty-list', $c); + } else { + assertType('array{}', $c); + } + + assertType('list', $c); + } + + function doBar() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_RECURSIVE) <= 1) === true); + assertType('list', $list); + } + + function doIf():void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, COUNT_RECURSIVE) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + + function countModeInt(int $i):void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, $i) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10283.php b/tests/PHPStan/Analyser/nsrt/bug-10283.php new file mode 100644 index 0000000000..e2cb63e31e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10283.php @@ -0,0 +1,25 @@ +x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +function testExtendedInterface(AnotherInterface $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +interface AnotherInterface extends SampleInterface +{ +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass implements SampleInterface {} + +class ValidSubClass extends SomeSubClass implements SampleInterface {} + +class InvalidClass implements SampleInterface {} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php new file mode 100644 index 0000000000..6489de2dcc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php @@ -0,0 +1,52 @@ +x); + assertType('string', $this->y); + assertType('*ERROR*', $this->z); + } +} + +/** + * @phpstan-require-extends SomeClass + */ +trait anotherTrait +{ +} + +class SomeClass { + public int $x = 1; + protected string $y = 'foo'; + private array $z = []; +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass { + use myTrait; +} + +class ValidSubClass extends SomeSubClass { + use myTrait; +} + +class InvalidClass { + use anotherTrait; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php new file mode 100644 index 0000000000..f4b26cea11 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php @@ -0,0 +1,54 @@ +foo(); + $this->doesNotExist(); + } +} + +interface SomeInterface +{ + public function foo(): string; +} + +class SomeClass implements SomeInterface { + use myTrait; + + public int $x; + protected string $y; + private array $z = []; + + public function foo(): string + { + return "hallo"; + } +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('string', $test->foo()); +} + +class ValidImplements implements SomeInterface { + public function foo(): string + { + return "hallo"; + } +} + +class InvalidClass { + use myTrait; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10317.php b/tests/PHPStan/Analyser/nsrt/bug-10317.php new file mode 100644 index 0000000000..1ccd87d41a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10317.php @@ -0,0 +1,14 @@ +optionalKey ?? null; + + assertType('int|null', $valueObject); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10338.php b/tests/PHPStan/Analyser/nsrt/bug-10338.php new file mode 100644 index 0000000000..cb9103eae0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10338.php @@ -0,0 +1,12 @@ + $options Array of options. Possible keys are: + * + * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array + * If an array, the following keys can be used: + * + * - `config` - Used to store the cached element in a custom cache configuration. + * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_` + * + * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element. + * Defaults to false. + * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions. + * - `plugin` - setting to false will force to use the application's element from plugin templates, when the + * plugin has element with same name. Defaults to true + * @return string Rendered Element + * @psalm-param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options + */ + public function element(string $name, array $data = [], array $options = []): string + { + assertType('array|true', $options['cache']); + $options += ['callbacks' => false, 'cache' => null, 'plugin' => null, 'ignoreMissing' => false]; + assertType('array|true|null', $options['cache']); + if (isset($options['cache'])) { + $options['cache'] = $this->_elementCache( + $name, + $data, + array_diff_key($options, ['callbacks' => false, 'plugin' => null, 'ignoreMissing' => null]) + ); + assertType('array{key: string, config: string}', $options['cache']); + } else { + assertType('null', $options['cache']); + } + assertType('array{key: string, config: string}|null', $options['cache']); + + $pluginCheck = $options['plugin'] !== false; + $file = $this->_getElementFileName($name, $pluginCheck); + if ($file && $options['cache']) { + assertType('array{key: string, config: string}', $options['cache']); + return $this->cache(function (): void { + echo ''; + }, $options['cache']); + } + + return $file; + } + + /** + * @param string $name + * @param bool $pluginCheck + */ + protected function _getElementFileName(string $name, bool $pluginCheck): string + { + return $name; + } + + /** + * @param callable $block The block of code that you want to cache the output of. + * @param array $options The options defining the cache key etc. + * @return string The rendered content. + * @throws \InvalidArgumentException When $options is lacking a 'key' option. + */ + public function cache(callable $block, array $options = []): string + { + $options += ['key' => '', 'config' => []]; + if (empty($options['key'])) { + throw new \InvalidArgumentException('Cannot cache content with an empty key'); + } + /** @var string $result */ + $result = $options['key']; + if ($result) { + return $result; + } + + $bufferLevel = ob_get_level(); + ob_start(); + + try { + $block(); + } catch (\Throwable $exception) { + while (ob_get_level() > $bufferLevel) { + ob_end_clean(); + } + + throw $exception; + } + + $result = (string)ob_get_clean(); + + return $result; + } + + /** + * Generate the cache configuration options for an element. + * + * @param string $name Element name + * @param array $data Data + * @param array $options Element options + * @return array Element Cache configuration. + * @psalm-return array{key:string, config:string} + */ + protected function _elementCache(string $name, array $data, array $options): array + { + [$plugin, $name] = explode(':', $name, 2); + + $pluginKey = null; + if ($plugin) { + $pluginKey = str_replace('/', '_', $plugin); + } + $elementKey = str_replace(['\\', '/'], '_', $name); + + $cache = $options['cache']; + unset($options['cache']); + $keys = array_merge( + [$pluginKey, $elementKey], + array_keys($options), + array_keys($data) + ); + $config = [ + 'config' => [], + 'key' => implode('_', array_keys($keys)), + ]; + if (is_array($cache)) { + $config = $cache + $config; + } + $config['key'] = 'element_' . $config['key']; + + /** @var array{config: string, key: string} */ + return $config; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10442.php b/tests/PHPStan/Analyser/nsrt/bug-10442.php new file mode 100644 index 0000000000..d8e2f7612c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10442.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug10445; + +use function PHPStan\Testing\assertType; + +enum Error { + case A; + case B; + case C; +} + +/** + * @template T of array + */ +class Response +{ + /** + * @param ?T $data + * @param Error[] $errors + */ + public function __construct( + public ?array $data, + public array $errors = [], + ) { + } + + /** + * @return array{ + * result: ?T, + * errors?: string[], + * } + */ + public function format(): array + { + $output = [ + 'result' => $this->data, + ]; + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + if (count($this->errors) > 0) { + $output['errors'] = array_map(fn ($e) => $e->name, $this->errors); + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors: non-empty-array<'A'|'B'|'C'>}", $output); + } else { + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + } + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors?: non-empty-array<'A'|'B'|'C'>}", $output); + return $output; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10468.php b/tests/PHPStan/Analyser/nsrt/bug-10468.php new file mode 100644 index 0000000000..8a3e30e970 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10468.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug10473; + +use ArrayAccess; +use function PHPStan\Testing\assertType; + +/** + * @template TRow of array + */ +class Rows +{ + + /** + * @param list $rowsData + */ + public function __construct(private array $rowsData) + {} + + /** + * @return Row|NULL + */ + public function getByIndex(int $index): ?Row + { + return isset($this->rowsData[$index]) + ? new Row($this->rowsData[$index]) + : NULL; + } +} + +/** + * @template TRow of array + * @implements ArrayAccess, value-of> + */ +class Row implements ArrayAccess +{ + + /** + * @param TRow $data + */ + public function __construct(private array $data) + {} + + /** + * @param key-of $key + */ + public function offsetExists($key): bool + { + return isset($this->data[$key]); + } + + /** + * @template TKey of key-of + * @param TKey $key + * @return TRow[TKey] + */ + public function offsetGet($key): mixed + { + return $this->data[$key]; + } + + public function offsetSet($key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function offsetUnset($key): void + { + unset($this->data[$key]); + } + + /** + * @return TRow + */ + public function toArray(): array + { + return $this->data; + } + +} + +class Foo +{ + + /** @param Rows}> $rows */ + public function doFoo(Rows $rows): void + { + assertType('Bug10473\Rows}>', $rows); + + $row = $rows->getByIndex(0); + + if ($row !== NULL) { + assertType('Bug10473\Row}>', $row); + $fooFromRow = $row['foo']; + + assertType('int<0, max>', $fooFromRow); + assertType('array{foo: int<0, max>}', $row->toArray()); + } + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10477.php b/tests/PHPStan/Analyser/nsrt/bug-10477.php new file mode 100644 index 0000000000..c97672bf98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10477.php @@ -0,0 +1,28 @@ +foo); + assertType('$this(Bug10477\A)', $this); + (new B())->foo($this); + assertType('mixed', $this->foo); + assertType('$this(Bug10477\A)', $this); + if (isset($this->data['test'])) { + $this->foo = $this->data['test']; + } + } +} + +class B +{ + public function foo(mixed &$var): void {} +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10528.php b/tests/PHPStan/Analyser/nsrt/bug-10528.php new file mode 100644 index 0000000000..07fe77ade0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10528.php @@ -0,0 +1,17 @@ +', $pos); + + $sub = substr($string, 0, $pos); + assert($pos !== FALSE); + $sub = substr($string, 0, $pos); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-10566.php b/tests/PHPStan/Analyser/nsrt/bug-10566.php new file mode 100644 index 0000000000..2fb46c5a7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10566.php @@ -0,0 +1,171 @@ +running = true; + + while ($this->running) { + assertType('true', $this->running); + call_user_func(function () { + $this->stop(); + }); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function stop(): void + { + $this->running = false; + } + + public function run2(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run3(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () { + $s = new self(); + $s->stop(); + }); + assertType('true', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run4(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + $cb = function () use ($s) { + $s = new self(); + $s->stop(); + }; + assertType('true', $s->running); + call_user_func($cb); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run5(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + $cb = function () { + $this->stop(); + }; + assertType('true', $this->running); + call_user_func($cb); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function run6(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + (function () use ($s) { + $s = new self(); + $s->stop(); + })(); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run7(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + (function () { + $this->stop(); + })(); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + function run8(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(static function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10627.php b/tests/PHPStan/Analyser/nsrt/bug-10627.php new file mode 100644 index 0000000000..17579ec52c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10627.php @@ -0,0 +1,95 @@ + $list + * @return void + */ + public function sayHello9(array $list): void + { + krsort($list); + assertType("array, string>", $list); + } + + public function sayHello10(): void + { + $list = ['a' => 'A', 'c' => 'C', 'b' => 'B']; + krsort($list); + assertType("array{a: 'A', c: 'C', b: 'B'}", $list); + assertType('false', array_is_list($list)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10650.php b/tests/PHPStan/Analyser/nsrt/bug-10650.php new file mode 100644 index 0000000000..97ce54a9af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10650.php @@ -0,0 +1,33 @@ + $distPoints + */ + public function repro(array $distPoints): void + { + $ranges = []; + $pointPrev = null; + foreach ($distPoints as $distPoint) { + if ($pointPrev !== null) { + $ranges[] = 'x'; + } + $pointPrev = $distPoint; + } + + assertType('list<\'x\'>', $ranges); + + foreach (array_keys($ranges) as $key) { + if (mt_rand() === 0) { + unset($ranges[$key]); + } + } + + assertType('array, \'x\'>', $ranges); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10653.php b/tests/PHPStan/Analyser/nsrt/bug-10653.php new file mode 100644 index 0000000000..fc6642a229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10653.php @@ -0,0 +1,13 @@ +mayFail(); + assertType('stdClass|false', $value); + $value = $a->throwOnFailure($value); + assertType(stdClass::class, $value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10685.php b/tests/PHPStan/Analyser/nsrt/bug-10685.php new file mode 100644 index 0000000000..17f51f2b26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10685.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug10685; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @template A + * @param A $value + * @return A + */ + function identity(mixed $value): mixed + { + return $value; + } + + public function doFoo(): void + { + assertType('array{1|2|3, 1|2|3, 1|2|3}', array_map(fn($i) => $i, [1, 2, 3])); + assertType('array{1, 2, 3}', array_map($this->identity(...), [1, 2, 3])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10699.php b/tests/PHPStan/Analyser/nsrt/bug-10699.php new file mode 100644 index 0000000000..de06986e90 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10699.php @@ -0,0 +1,49 @@ + */ + const DATA = [ + 'af' => [ + 'code' => 'af', + 'english' => "Afrikaans", + 'local' => "Afrikaans", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'am' => [ + 'code' => 'am', + 'english' => "Amharic", + 'local' => "አማርኛ", + 'rtl' => false, + 'country' => 'et', + 'variant' => false, + ], + 'ar' => [ + 'code' => 'ar', + 'english' => "Arabic", + 'local' => "العربية‏", + 'rtl' => true, + 'country' => 'sa', + 'variant' => false, + ], + 'az' => [ + 'code' => 'az', + 'english' => "Azerbaijani", + 'local' => "Azərbaycan dili", + 'rtl' => false, + 'country' => 'az', + 'variant' => false, + ], + 'ba' => [ + 'code' => 'ba', + 'english' => "Bashkir", + 'local' => "башҡорт теле", + 'rtl' => false, + 'country' => 'ru', + 'variant' => false, + ], + 'be' => [ + 'code' => 'be', + 'english' => "Belarusian", + 'local' => "Беларуская", + 'rtl' => false, + 'country' => 'by', + 'variant' => false, + ], + 'bg' => [ + 'code' => 'bg', + 'english' => "Bulgarian", + 'local' => "Български", + 'rtl' => false, + 'country' => 'bg', + 'variant' => false, + ], + 'bn' => [ + 'code' => 'bn', + 'english' => "Bengali", + 'local' => "বাংলা", + 'rtl' => false, + 'country' => 'bd', + 'variant' => false, + ], + 'br' => [ + 'code' => 'br', + 'english' => "Brazilian Portuguese", + 'local' => "Português Brasileiro", + 'rtl' => false, + 'country' => 'br', + 'variant' => false, + ], + 'bs' => [ + 'code' => 'bs', + 'english' => "Bosnian", + 'local' => "Bosanski", + 'rtl' => false, + 'country' => 'ba', + 'variant' => false, + ], + 'ca' => [ + 'code' => 'ca', + 'english' => "Catalan", + 'local' => "Català", + 'rtl' => false, + 'country' => 'es-ca', + 'variant' => false, + ], + 'co' => [ + 'code' => 'co', + 'english' => "Corsican", + 'local' => "Corsu", + 'rtl' => false, + 'country' => 'fr-co', + 'variant' => false, + ], + 'cs' => [ + 'code' => 'cs', + 'english' => "Czech", + 'local' => "Čeština", + 'rtl' => false, + 'country' => 'cz', + 'variant' => false, + ], + 'cy' => [ + 'code' => 'cy', + 'english' => "Welsh", + 'local' => "Cymraeg", + 'rtl' => false, + 'country' => 'gb-wls', + 'variant' => false, + ], + 'da' => [ + 'code' => 'da', + 'english' => "Danish", + 'local' => "Dansk", + 'rtl' => false, + 'country' => 'dk', + 'variant' => false, + ], + 'de' => [ + 'code' => 'de', + 'english' => "German", + 'local' => "Deutsch", + 'rtl' => false, + 'country' => 'de', + 'variant' => false, + ], + 'el' => [ + 'code' => 'el', + 'english' => "Greek", + 'local' => "Ελληνικά", + 'rtl' => false, + 'country' => 'gr', + 'variant' => false, + ], + 'en' => [ + 'code' => 'en', + 'english' => "English", + 'local' => "English", + 'rtl' => false, + 'country' => 'gb', + 'variant' => false, + ], + 'eo' => [ + 'code' => 'eo', + 'english' => "Esperanto", + 'local' => "Esperanto", + 'rtl' => false, + 'country' => 'eo', + 'variant' => false, + ], + 'es' => [ + 'code' => 'es', + 'english' => "Spanish", + 'local' => "Español", + 'rtl' => false, + 'country' => 'es', + 'variant' => false, + ], + 'et' => [ + 'code' => 'et', + 'english' => "Estonian", + 'local' => "Eesti", + 'rtl' => false, + 'country' => 'ee', + 'variant' => false, + ], + 'eu' => [ + 'code' => 'eu', + 'english' => "Basque", + 'local' => "Euskara", + 'rtl' => false, + 'country' => 'eus', + 'variant' => false, + ], + 'fa' => [ + 'code' => 'fa', + 'english' => "Persian", + 'local' => "فارسی", + 'rtl' => true, + 'country' => 'ir', + 'variant' => false, + ], + 'fi' => [ + 'code' => 'fi', + 'english' => "Finnish", + 'local' => "Suomi", + 'rtl' => false, + 'country' => 'fi', + 'variant' => false, + ], + 'fj' => [ + 'code' => 'fj', + 'english' => "Fijian", + 'local' => "Vosa Vakaviti", + 'rtl' => false, + 'country' => 'fj', + 'variant' => false, + ], + 'fl' => [ + 'code' => 'fl', + 'english' => "Filipino", + 'local' => "Filipino", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'fr' => [ + 'code' => 'fr', + 'english' => "French", + 'local' => "Français", + 'rtl' => false, + 'country' => 'fr', + 'variant' => false, + ], + 'fy' => [ + 'code' => 'fy', + 'english' => "Western Frisian", + 'local' => "frysk", + 'rtl' => false, + 'country' => 'nl', + 'variant' => false, + ], + 'ga' => [ + 'code' => 'ga', + 'english' => "Irish", + 'local' => "Gaeilge", + 'rtl' => false, + 'country' => 'ie', + 'variant' => false, + ], + 'gd' => [ + 'code' => 'gd', + 'english' => "Scottish Gaelic", + 'local' => "Gàidhlig", + 'rtl' => false, + 'country' => 'gb-sct', + 'variant' => false, + ], + 'gl' => [ + 'code' => 'gl', + 'english' => "Galician", + 'local' => "Galego", + 'rtl' => false, + 'country' => 'es-ga', + 'variant' => false, + ], + 'gu' => [ + 'code' => 'gu', + 'english' => "Gujarati", + 'local' => "ગુજરાતી", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ha' => [ + 'code' => 'ha', + 'english' => "Hausa", + 'local' => "هَوُسَ", + 'rtl' => false, + 'country' => 'ne', + 'variant' => false, + ], + 'he' => [ + 'code' => 'he', + 'english' => "Hebrew", + 'local' => "עברית", + 'rtl' => true, + 'country' => 'il', + 'variant' => false, + ], + 'hi' => [ + 'code' => 'hi', + 'english' => "Hindi", + 'local' => "हिंदी", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'hr' => [ + 'code' => 'hr', + 'english' => "Croatian", + 'local' => "Hrvatski", + 'rtl' => false, + 'country' => 'hr', + 'variant' => false, + ], + 'ht' => [ + 'code' => 'ht', + 'english' => "Haitian Creole", + 'local' => "Kreyòl ayisyen", + 'rtl' => false, + 'country' => 'ht', + 'variant' => false, + ], + 'hu' => [ + 'code' => 'hu', + 'english' => "Hungarian", + 'local' => "Magyar", + 'rtl' => false, + 'country' => 'hu', + 'variant' => false, + ], + 'hw' => [ + 'code' => 'hw', + 'english' => "Hawaiian", + 'local' => "‘Ōlelo Hawai‘i", + 'rtl' => false, + 'country' => 'hw', + 'variant' => false, + ], + 'hy' => [ + 'code' => 'hy', + 'english' => "Armenian", + 'local' => "հայերեն", + 'rtl' => false, + 'country' => 'am', + 'variant' => false, + ], + 'id' => [ + 'code' => 'id', + 'english' => "Indonesian", + 'local' => "Bahasa Indonesia", + 'rtl' => false, + 'country' => 'id', + 'variant' => false, + ], + 'ig' => [ + 'code' => 'ig', + 'english' => "Igbo", + 'local' => "Igbo", + 'rtl' => false, + 'country' => 'ne', + 'variant' => false, + ], + 'is' => [ + 'code' => 'is', + 'english' => "Icelandic", + 'local' => "Íslenska", + 'rtl' => false, + 'country' => 'is', + 'variant' => false, + ], + 'it' => [ + 'code' => 'it', + 'english' => "Italian", + 'local' => "Italiano", + 'rtl' => false, + 'country' => 'it', + 'variant' => false, + ], + 'ja' => [ + 'code' => 'ja', + 'english' => "Japanese", + 'local' => "日本語", + 'rtl' => false, + 'country' => 'jp', + 'variant' => false, + ], + 'jv' => [ + 'code' => 'jv', + 'english' => "Javanese", + 'local' => "Wong Jawa", + 'rtl' => false, + 'country' => 'id', + 'variant' => false, + ], + 'ka' => [ + 'code' => 'ka', + 'english' => "Georgian", + 'local' => "ქართული", + 'rtl' => false, + 'country' => 'ge', + 'variant' => false, + ], + 'kk' => [ + 'code' => 'kk', + 'english' => "Kazakh", + 'local' => "Қазақша", + 'rtl' => false, + 'country' => 'kz', + 'variant' => false, + ], + 'km' => [ + 'code' => 'km', + 'english' => "Central Khmer", + 'local' => "ភាសាខ្មែរ", + 'rtl' => false, + 'country' => 'kh', + 'variant' => false, + ], + 'kn' => [ + 'code' => 'kn', + 'english' => "Kannada", + 'local' => "ಕನ್ನಡ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ko' => [ + 'code' => 'ko', + 'english' => "Korean", + 'local' => "한국어", + 'rtl' => false, + 'country' => 'kr', + 'variant' => false, + ], + 'ku' => [ + 'code' => 'ku', + 'english' => "Kurdish", + 'local' => "كوردی", + 'rtl' => true, + 'country' => 'iq', + 'variant' => false, + ], + 'ky' => [ + 'code' => 'ky', + 'english' => "Kyrgyz", + 'local' => "кыргызча", + 'rtl' => false, + 'country' => 'kg', + 'variant' => false, + ], + 'la' => [ + 'code' => 'la', + 'english' => "Latin", + 'local' => "Latine", + 'rtl' => false, + 'country' => 'it', + 'variant' => false, + ], + 'lb' => [ + 'code' => 'lb', + 'english' => "Luxembourgish", + 'local' => "Lëtzebuergesch", + 'rtl' => false, + 'country' => 'lu', + 'variant' => false, + ], + 'lo' => [ + 'code' => 'lo', + 'english' => "Lao", + 'local' => "ພາສາລາວ", + 'rtl' => false, + 'country' => 'la', + 'variant' => false, + ], + 'lt' => [ + 'code' => 'lt', + 'english' => "Lithuanian", + 'local' => "Lietuvių", + 'rtl' => false, + 'country' => 'lt', + 'variant' => false, + ], + 'lv' => [ + 'code' => 'lv', + 'english' => "Latvian", + 'local' => "Latviešu", + 'rtl' => false, + 'country' => 'lv', + 'variant' => false, + ], + 'lg' => [ + 'code' => 'lg', + 'english' => "Luganda", + 'local' => "Oluganda", + 'rtl' => false, + 'country' => 'ug', + 'variant' => false, + ], + 'mg' => [ + 'code' => 'mg', + 'english' => "Malagasy", + 'local' => "Malagasy", + 'rtl' => false, + 'country' => 'mg', + 'variant' => false, + ], + 'mi' => [ + 'code' => 'mi', + 'english' => "Māori", + 'local' => "te reo Māori", + 'rtl' => false, + 'country' => 'nz', + 'variant' => false, + ], + 'mk' => [ + 'code' => 'mk', + 'english' => "Macedonian", + 'local' => "Македонски", + 'rtl' => false, + 'country' => 'mk', + 'variant' => false, + ], + 'ml' => [ + 'code' => 'ml', + 'english' => "Malayalam", + 'local' => "മലയാളം", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'mn' => [ + 'code' => 'mn', + 'english' => "Mongolian", + 'local' => "Монгол", + 'rtl' => false, + 'country' => 'mn', + 'variant' => false, + ], + 'mr' => [ + 'code' => 'mr', + 'english' => "Marathi", + 'local' => "मराठी", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ms' => [ + 'code' => 'ms', + 'english' => "Malay", + 'local' => "Bahasa Melayu", + 'rtl' => false, + 'country' => 'my', + 'variant' => false, + ], + 'mt' => [ + 'code' => 'mt', + 'english' => "Maltese", + 'local' => "Malti", + 'rtl' => false, + 'country' => 'mt', + 'variant' => false, + ], + 'my' => [ + 'code' => 'my', + 'english' => "Burmese", + 'local' => "မျန္မာစာ", + 'rtl' => false, + 'country' => 'mm', + 'variant' => false, + ], + 'ne' => [ + 'code' => 'ne', + 'english' => "Nepali", + 'local' => "नेपाली", + 'rtl' => false, + 'country' => 'np', + 'variant' => false, + ], + 'nl' => [ + 'code' => 'nl', + 'english' => "Dutch", + 'local' => "Nederlands", + 'rtl' => false, + 'country' => 'nl', + 'variant' => false, + ], + 'no' => [ + 'code' => 'no', + 'english' => "Norwegian", + 'local' => "Norsk", + 'rtl' => false, + 'country' => 'no', + 'variant' => false, + ], + 'ny' => [ + 'code' => 'ny', + 'english' => "Chichewa", + 'local' => "chiCheŵa", + 'rtl' => false, + 'country' => 'mw', + 'variant' => false, + ], + 'pa' => [ + 'code' => 'pa', + 'english' => "Punjabi", + 'local' => "ਪੰਜਾਬੀ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'pl' => [ + 'code' => 'pl', + 'english' => "Polish", + 'local' => "Polski", + 'rtl' => false, + 'country' => 'pl', + 'variant' => false, + ], + 'ps' => [ + 'code' => 'ps', + 'english' => "Pashto", + 'local' => "پښتو", + 'rtl' => true, + 'country' => 'pk', + 'variant' => false, + ], + 'pt' => [ + 'code' => 'pt', + 'english' => "Portuguese", + 'local' => "Português", + 'rtl' => false, + 'country' => 'pt', + 'variant' => false, + ], + 'ro' => [ + 'code' => 'ro', + 'english' => "Romanian", + 'local' => "Română", + 'rtl' => false, + 'country' => 'ro', + 'variant' => false, + ], + 'ru' => [ + 'code' => 'ru', + 'english' => "Russian", + 'local' => "Русский", + 'rtl' => false, + 'country' => 'ru', + 'variant' => false, + ], + 'sd' => [ + 'code' => 'sd', + 'english' => "Sindhi", + 'local' => "سنڌي، سندھی, सिन्धी", + 'rtl' => false, + 'country' => 'pk', + 'variant' => false, + ], + 'si' => [ + 'code' => 'si', + 'english' => "Sinhalese", + 'local' => "සිංහල", + 'rtl' => false, + 'country' => 'lk', + 'variant' => false, + ], + 'sk' => [ + 'code' => 'sk', + 'english' => "Slovak", + 'local' => "Slovenčina", + 'rtl' => false, + 'country' => 'sk', + 'variant' => false, + ], + 'sl' => [ + 'code' => 'sl', + 'english' => "Slovenian", + 'local' => "Slovenščina", + 'rtl' => false, + 'country' => 'si', + 'variant' => false, + ], + 'sm' => [ + 'code' => 'sm', + 'english' => "Samoan", + 'local' => "gagana fa'a Samoa", + 'rtl' => false, + 'country' => 'ws', + 'variant' => false, + ], + 'sn' => [ + 'code' => 'sn', + 'english' => "Shona", + 'local' => "chiShona", + 'rtl' => false, + 'country' => 'zw', + 'variant' => false, + ], + 'so' => [ + 'code' => 'so', + 'english' => "Somali", + 'local' => "Soomaaliga", + 'rtl' => false, + 'country' => 'so', + 'variant' => false, + ], + 'sq' => [ + 'code' => 'sq', + 'english' => "Albanian", + 'local' => "Shqip", + 'rtl' => false, + 'country' => 'al', + 'variant' => false, + ], + 'sr' => [ + 'code' => 'sr', + 'english' => "Serbian (Cyrillic)", + 'local' => "Српски", + 'rtl' => false, + 'country' => 'rs', + 'variant' => false, + ], + 'st' => [ + 'code' => 'st', + 'english' => "Southern Sotho", + 'local' => "seSotho", + 'rtl' => false, + 'country' => 'ng', + 'variant' => false, + ], + 'su' => [ + 'code' => 'su', + 'english' => "Sundanese", + 'local' => "Sundanese", + 'rtl' => false, + 'country' => 'sd', + 'variant' => false, + ], + 'sv' => [ + 'code' => 'sv', + 'english' => "Swedish", + 'local' => "Svenska", + 'rtl' => false, + 'country' => 'se', + 'variant' => false, + ], + 'sw' => [ + 'code' => 'sw', + 'english' => "Swahili", + 'local' => "Kiswahili", + 'rtl' => false, + 'country' => 'ke', + 'variant' => false, + ], + 'ta' => [ + 'code' => 'ta', + 'english' => "Tamil", + 'local' => "தமிழ்", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'te' => [ + 'code' => 'te', + 'english' => "Telugu", + 'local' => "తెలుగు", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'tg' => [ + 'code' => 'tg', + 'english' => "Tajik", + 'local' => "Тоҷикӣ", + 'rtl' => false, + 'country' => 'tj', + 'variant' => false, + ], + 'th' => [ + 'code' => 'th', + 'english' => "Thai", + 'local' => "ภาษาไทย", + 'rtl' => false, + 'country' => 'th', + 'variant' => false, + ], + 'tl' => [ + 'code' => 'tl', + 'english' => "Tagalog", + 'local' => "Tagalog", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'to' => [ + 'code' => 'to', + 'english' => "Tongan", + 'local' => "faka-Tonga", + 'rtl' => false, + 'country' => 'to', + 'variant' => false, + ], + 'tr' => [ + 'code' => 'tr', + 'english' => "Turkish", + 'local' => "Türkçe", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'tt' => [ + 'code' => 'tt', + 'english' => "Tatar", + 'local' => "Tatar", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'tw' => [ + 'code' => 'tw', + 'english' => "Traditional Chinese", + 'local' => "中文 (繁體)", + 'rtl' => false, + 'country' => 'tw', + 'variant' => false, + ], + 'ty' => [ + 'code' => 'ty', + 'english' => "Tahitian", + 'local' => "te reo Tahiti, te reo Māʼohi", + 'rtl' => false, + 'country' => 'pf', + 'variant' => false, + ], + 'uk' => [ + 'code' => 'uk', + 'english' => "Ukrainian", + 'local' => "Українська", + 'rtl' => false, + 'country' => 'ua', + 'variant' => false, + ], + 'ur' => [ + 'code' => 'ur', + 'english' => "Urdu", + 'local' => "اردو", + 'rtl' => true, + 'country' => 'pk', + 'variant' => false, + ], + 'uz' => [ + 'code' => 'uz', + 'english' => "Uzbek", + 'local' => "O'zbek", + 'rtl' => false, + 'country' => 'uz', + 'variant' => false, + ], + 'vi' => [ + 'code' => 'vi', + 'english' => "Vietnamese", + 'local' => "Tiếng Việt", + 'rtl' => false, + 'country' => 'vn', + 'variant' => false, + ], + 'xh' => [ + 'code' => 'xh', + 'english' => "Xhosa", + 'local' => "isiXhosa", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'yi' => [ + 'code' => 'yi', + 'english' => "Yiddish", + 'local' => "ייִדיש", + 'rtl' => false, + 'country' => 'il', + 'variant' => false, + ], + 'yo' => [ + 'code' => 'yo', + 'english' => "Yoruba", + 'local' => "Yorùbá", + 'rtl' => false, + 'country' => 'ng', + 'variant' => false, + ], + 'zh' => [ + 'code' => 'zh', + 'english' => "Simplified Chinese", + 'local' => "中文 (简体)", + 'rtl' => false, + 'country' => 'cn', + 'variant' => false, + ], + 'zu' => [ + 'code' => 'zu', + 'english' => "Zulu", + 'local' => "isiZulu", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'hm' => [ + 'code' => 'hm', + 'english' => "Hmong", + 'local' => "Hmoob", + 'rtl' => false, + 'country' => 'hmn', + 'variant' => false, + ], + 'cb' => [ + 'code' => 'cb', + 'english' => "Cebuano", + 'local' => "Sugbuanon", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'or' => [ + 'code' => 'or', + 'english' => "Odia", + 'local' => "ଓଡ଼ିଆ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'tk' => [ + 'code' => 'tk', + 'english' => "Turkmen", + 'local' => "Türkmen", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'ug' => [ + 'code' => 'ug', + 'english' => "Uyghur", + 'local' => "ئۇيغۇر", + 'rtl' => true, + 'country' => 'uig', + 'variant' => false, + ], + 'fc' => [ + 'code' => 'fc', + 'english' => "French (Canada)", + 'local' => "Français (Canada)", + 'rtl' => false, + 'country' => 'ca', + 'variant' => true, + ], + 'as' => [ + 'code' => 'as', + 'english' => "Assamese", + 'local' => "অসমীয়া", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'sa' => [ + 'code' => 'sa', + 'english' => "Serbian (Latin)", + 'local' => "Srpski", + 'rtl' => false, + 'country' => 'rs', + 'variant' => false, + ], + 'om' => [ + 'code' => 'om', + 'english' => "Oromo", + 'local' => "Afaan Oromoo", + 'rtl' => false, + 'country' => 'et', + 'variant' => false, + ], + 'iu' => [ + 'code' => 'iu', + 'english' => "Inuktitut", + 'local' => "ᐃᓄᒃᑎᑐᑦ", + 'rtl' => false, + 'country' => 'ca', + 'variant' => false, + ], + 'ti' => [ + 'code' => 'ti', + 'english' => "Tigrinya", + 'local' => "ቲግሪንያ", + 'rtl' => false, + 'country' => 'er', + 'variant' => false, + ], + 'bm' => [ + 'code' => 'bm', + 'english' => "Bambara", + 'local' => "Bamanankan", + 'rtl' => false, + 'country' => 'ml', + 'variant' => false, + ], + 'bo' => [ + 'code' => 'bo', + 'english' => "Tibetan", + 'local' => "བོད་ཡིག", + 'rtl' => false, + 'country' => 'cn', + 'variant' => false, + ], + 'ak' => [ + 'code' => 'ak', + 'english' => "Akan", + 'local' => "Baoulé", + 'rtl' => false, + 'country' => 'gh', + 'variant' => false, + ], + 'rw' => [ + 'code' => 'rw', + 'english' => "Kinyarwanda", + 'local' => "Kinyarwanda", + 'rtl' => false, + 'country' => 'rw', + 'variant' => false, + ], + 'kb' => [ + 'code' => 'kb', + 'english' => "Kurdish (Sorani)", + 'local' => "سۆرانی", + 'rtl' => true, + 'country' => 'iq', + 'variant' => false, + ], + 'fo' => [ + 'code' => 'fo', + 'english' => "Faroese", + 'local' => "Føroyskt", + 'rtl' => false, + 'country' => 'fo', + 'variant' => false, + ] + ]; +} + +function test(string $code): void +{ + $country = Languages::DATA[$code]['country']; + + if ($country === 'fo' || $country === 'Faroese' || $country === 'Føroyskt') { + // foo + } else { + assertType('(bool|(literal-string&non-falsy-string))', $country); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-10721.php b/tests/PHPStan/Analyser/nsrt/bug-10721.php new file mode 100644 index 0000000000..c82d2298f2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10721.php @@ -0,0 +1,100 @@ + + */ + public function retrieve(?int $limit = 20): array + { + $list = [ + 'zib', + 'zib 2', + 'zeit im bild', + 'soko', + 'landkrimi', + 'tatort', + ]; + + assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list); + shuffle($list); + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", $list); + + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", array_slice($list, 0, max($limit, 1))); + return array_slice($list, 0, max($limit, 1)); + } + + public function listVariants(): void + { + $arr = [ + 2 => 'zib', + 4 => 'zib 2', + ]; + + assertType("array{2: 'zib', 4: 'zib 2'}", $arr); + shuffle($arr); + assertType("non-empty-list<'zib'|'zib 2'>", $arr); + + $list = [ + 'zib', + 'zib 2', + ]; + + assertType("array{'zib', 'zib 2'}", $list); + shuffle($list); + assertType("non-empty-list<'zib'|'zib 2'>", $list); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 1)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 1)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 2)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 2)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 2)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 2)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3)); + + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, -1, 3, true)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, true)); + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 1, 3, true)); // could be non-empty-array + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 2, 3, true)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3, false)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, false)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3, false)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3, false)); + } + + /** + * @param array $strings + * @param 0|1 $maybeZero + */ + public function arrayVariants(array $strings, $maybeZero): void + { + assertType("array", $strings); + assertType("list", array_slice($strings, 0)); + assertType("list", array_slice($strings, 1)); + assertType("list", array_slice($strings, $maybeZero)); + + if (count($strings) > 0) { + assertType("non-empty-array", $strings); + assertType("non-empty-list", array_slice($strings, 0)); + assertType("list", array_slice($strings, 1)); + assertType("list", array_slice($strings, $maybeZero)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10834.php b/tests/PHPStan/Analyser/nsrt/bug-10834.php new file mode 100644 index 0000000000..8e9050ac4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10834.php @@ -0,0 +1,23 @@ + $b + */ + public function doFoo($b): void + { + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + } + + /** + * @param int|false $b + */ + public function doFoo2($b): void + { + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10893.php b/tests/PHPStan/Analyser/nsrt/bug-10893.php new file mode 100644 index 0000000000..0878d2f302 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10893.php @@ -0,0 +1,28 @@ +format('u')); + assertType('int', (int)$value->format('u')); + assertType('bool', (int)$value->format('u') !== 0); + + assertType('non-falsy-string&numeric-string', $value->format('v')); + assertType('int', (int)$value->format('v')); + assertType('bool', (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..caf95180ab --- /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('list{0?: string, 1?: non-falsy-string&numeric-string}', $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('list{0?: string, 1?: non-falsy-string&numeric-string}', $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('list{0?: string, 1?: non-falsy-string&numeric-string}', $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-11322.php b/tests/PHPStan/Analyser/nsrt/bug-11322.php new file mode 100644 index 0000000000..3be6f535ac --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11322.php @@ -0,0 +1,12 @@ + ['a' => 'b']]; + assertType("array{map: array{a: 'b'}}", $result); + usort($result['map'], fn (string $a, string $b) => $a <=> $b); + assertType("array{map: non-empty-list<'b'>}", $result); +} 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 @@ +', $r); +} + + +/** + * @return string + */ +function fooCallback(string $s) +{ + $r = preg_replace_callback('/^a/', function ($matches) { + return strtolower($matches[0]); + }, $s); + assertType('string|null', $r); + return $r; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11561.php b/tests/PHPStan/Analyser/nsrt/bug-11561.php new file mode 100644 index 0000000000..f6894d4724 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11561.php @@ -0,0 +1,80 @@ += 8.0 + +namespace Bug11561; + +use function PHPStan\Testing\assertType; +use DateTime; + +/** @param array{date: DateTime} $c */ +function main(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + assertType('array{date: DateTime, id: 1}', $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + + +/** @param array{date: DateTime} $c */ +function main2(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main3(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + } + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main4(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'y'; + } + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} diff --git a/tests/PHPStan/Analyser/data/bug-1157.php b/tests/PHPStan/Analyser/nsrt/bug-1157.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1157.php rename to tests/PHPStan/Analyser/nsrt/bug-1157.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11570.php b/tests/PHPStan/Analyser/nsrt/bug-11570.php new file mode 100644 index 0000000000..7a97062078 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11570.php @@ -0,0 +1,14 @@ + $var !== null); + assertType("array{one?: string, two?: string, three?: string}", $data); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11580.php b/tests/PHPStan/Analyser/nsrt/bug-11580.php new file mode 100644 index 0000000000..039a1895f5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11580.php @@ -0,0 +1,35 @@ + $criteria + * + * @return object[] The objects. + * @psalm-return list<\stdClass> + */ + function findBy(array $criteria): array + { + return [new \stdClass, new \stdCLass, new \stdClass, new \stdClass]; + } +} + +class Payload { + /** @var non-empty-list */ + public array $ids = ['one', 'two']; +} + +function doFoo() { + $payload = new Payload(); + + $fetcher = new Repository(); + $entries = $fetcher->findBy($payload->ids); + assertType('list', $entries); + assertType('int<0, max>', count($entries)); + assertType('int<1, max>', count($payload->ids)); + if (count($entries) !== count($payload->ids)) { + exit(); + } + + assertType('non-empty-list', $entries); + if (count($entries) > 3) { + throw new \RuntimeException(); + } + + assertType('non-empty-list', $entries); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11692.php b/tests/PHPStan/Analyser/nsrt/bug-11692.php new file mode 100644 index 0000000000..c1edbba1fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11692.php @@ -0,0 +1,24 @@ +', range(1, 9, .01)); + assertType('array{1, 4, 7}', range(1, 9, 3)); + + assertType('non-empty-list', range(1, 9999, .01)); + assertType('non-empty-list>', range(1, 9999, 3)); + + assertType('list', range(1, 9999, $floatOrInt)); + assertType('list', range(1, 9999, $floatOrInt)); + + assertType('list', range(1, 3, $i)); + assertType('list', range(1, 3, $f)); + + assertType('list', range(1, 9999, $i)); + assertType('list', range(1, 9999, $f)); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-11699.php b/tests/PHPStan/Analyser/nsrt/bug-11699.php new file mode 100644 index 0000000000..65eebc78a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11699.php @@ -0,0 +1,51 @@ +[\~,\?\.])~', $string, $match); + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo1():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?[\~,\?.])~', $string, $match); // dot in character class does not need to be escaped + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo2():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?.)~', $string, $match); + if ($result === 1) { + assertType("non-empty-string", $match['AB']); + } +} + + +function doFoo3():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?\.)~', $string, $match); + if ($result === 1) { + assertType("'.'", $match['AB']); + } +} + +function doFoo4():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?[^\~,\?\.])~', $string, $match); + if ($result === 1) { + assertType("non-empty-string", $match['AB']); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11703.php b/tests/PHPStan/Analyser/nsrt/bug-11703.php new file mode 100644 index 0000000000..ae1a91127b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11703.php @@ -0,0 +1,99 @@ + 'Some message about the alert.', + 'duration' => $duration, + 'severity' => 100, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert2.', + 'duration' => $duration, + 'severity' => 99, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert3.', + 'duration' => $duration, + 'severity' => 75, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert4.', + 'duration' => $duration, + 'severity' => 60, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert5.', + 'duration' => null, + 'severity' => 25, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert6.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert7.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert8.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert9.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + assertType('int<0, 9>', count($alerts)); + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert10.', + 'duration' => $duration, + 'severity' => 23, + ]; + } + + assertType('int<0, max>', count($alerts)); + if (count($alerts) === 0) { + return null; + } + + return true; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php new file mode 100644 index 0000000000..3dced2a08d --- /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-11724.php b/tests/PHPStan/Analyser/nsrt/bug-11724.php new file mode 100644 index 0000000000..baf4c01658 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11724.php @@ -0,0 +1,15 @@ + 'hello']; + assertType('true', array_key_exists($f, $a)); + if (array_key_exists($f, $a)) { + assertType('5.0', $f); + assertType("array{5: 'hello'}", $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11846.php b/tests/PHPStan/Analyser/nsrt/bug-11846.php new file mode 100644 index 0000000000..02ace75d07 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11846.php @@ -0,0 +1,28 @@ +', $outerList); + + foreach ($outerList as $key => $outerElement) { + $result = false; + + assertType('array{array{}}', $outerElement); + foreach ($outerElement as $innerElement) { + $result = true; + } + assertType('true', $result); + + } +} 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-11912.php b/tests/PHPStan/Analyser/nsrt/bug-11912.php new file mode 100644 index 0000000000..69d65ee01c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11912.php @@ -0,0 +1,14 @@ + $results + * @param list $names + */ +function appendResults(array $results, array $names): bool { + // Make sure 'names' comes first in array + $results = ['names' => $names] + $results; + \PHPStan\Testing\assertType("list", $results['names']); + return true; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11917.php b/tests/PHPStan/Analyser/nsrt/bug-11917.php new file mode 100644 index 0000000000..c09a7b61ab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11917.php @@ -0,0 +1,23 @@ + + */ +function generateList(string $name): array +{ + $a = ['a', 'b', 'c', $name]; + assertType('array{\'a\', \'b\', \'c\', string}', $a); + $b = ['d', 'e']; + assertType('array{\'d\', \'e\'}', $b); + + array_splice($a, 2, 0, $b); + assertType('array{\'a\', \'b\', \'d\', \'e\', \'c\', string}', $a); + + return $a; +} + +var_dump(generateList('John')); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11928.php b/tests/PHPStan/Analyser/nsrt/bug-11928.php new file mode 100644 index 0000000000..94317f690f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11928.php @@ -0,0 +1,68 @@ + 1, 3 => 2, 4 => 1]; + + $keys = array_keys($a, 1); // returns [2, 4] + assertType('list<2|3|4>', $keys); + + $keys = array_keys($a); // returns [2, 3, 4] + assertType('array{2, 3, 4}', $keys); +} + +/** + * @param array<1|2|3, 4|5|6> $unionKeyedArray + * @param 4|5 $fourOrFive + * @return void + */ +function doFooStrings($unionKeyedArray, $fourOrFive) { + $a = [2 => 'hi', 3 => '123', 'xy' => 5]; + $keys = array_keys($a, 1); + assertType("list<2|3|'xy'>", $keys); + + $keys = array_keys($a); + assertType("array{2, 3, 'xy'}", $keys); + + $keys = array_keys($unionKeyedArray, 1); + assertType("list<1|2|3>", $keys); // could be array{} + + $keys = array_keys($unionKeyedArray, 4); + assertType("list<1|2|3>", $keys); + + $keys = array_keys($unionKeyedArray, $fourOrFive); + assertType("list<1|2|3>", $keys); + + $keys = array_keys($unionKeyedArray); + assertType("list<1|2|3>", $keys); +} + +/** + * @param array $array + * @param list $list + * @param array $strings + * @return void + */ +function doFooBar(array $array, array $list, array $strings) { + $keys = array_keys($strings, "a", true); + assertType('list', $keys); + + $keys = array_keys($strings, "a", false); + assertType('list', $keys); + + $keys = array_keys($array, 1, true); + assertType('list', $keys); + + $keys = array_keys($array, 1, false); + assertType('list', $keys); + + $keys = array_keys($list, 1, true); + assertType('list>', $keys); + + $keys = array_keys($list, 1, true); + assertType('list>', $keys); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12065.php b/tests/PHPStan/Analyser/nsrt/bug-12065.php new file mode 100644 index 0000000000..e0f9353eec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12065.php @@ -0,0 +1,43 @@ + $key + * @param bool $preserveKeys + * + * @return void + */ + public function bar2( + string $key, + bool $preserveKeys, + ): void { + $format = $preserveKeys ? '%s' : '%d'; + + $_key = sprintf($format, $key); + assertType("string", $_key); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12077.php b/tests/PHPStan/Analyser/nsrt/bug-12077.php new file mode 100644 index 0000000000..07163ecaa1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12077.php @@ -0,0 +1,11 @@ += 8.3 + +namespace Bug12077; + +use ReflectionMethod; +use function PHPStan\Testing\assertType; + +function (): void { + $methodInfo = ReflectionMethod::createFromMethodName("Exception::getMessage"); + assertType(ReflectionMethod::class, $methodInfo); +}; diff --git a/tests/PHPStan/Analyser/data/bug-1209.php b/tests/PHPStan/Analyser/nsrt/bug-1209.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-1209.php rename to tests/PHPStan/Analyser/nsrt/bug-1209.php index fff8d13dff..4b4ce770d1 100644 --- a/tests/PHPStan/Analyser/data/bug-1209.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1209.php @@ -13,7 +13,7 @@ public function sayHello($value): void { $isArray = is_array($value); if($isArray){ - assertType('array', $value); + assertType('array', $value); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12107.php b/tests/PHPStan/Analyser/nsrt/bug-12107.php new file mode 100644 index 0000000000..1a2c839c05 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12107.php @@ -0,0 +1,43 @@ + $e2 */ + public function sayHello2(Throwable $e1, string $e2): void + { + if ($e1 instanceof $e2) { + return; + } + + + assertType('Throwable', $e1); + assertType('bool', $e1 instanceof $e2); // could be false + } + + public function sayHello3(Throwable $e1): void + { + if ($e1 instanceof LogicException) { + return; + } + + assertType('Throwable~LogicException', $e1); + assertType('false', $e1 instanceof LogicException); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12125.php b/tests/PHPStan/Analyser/nsrt/bug-12125.php new file mode 100644 index 0000000000..815e3cb701 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12125.php @@ -0,0 +1,37 @@ +|array $bug */ +$bug = []; + +assertType('stdClass|null', $bug['key'] ?? null); + +interface MyInterface { + /** @return array | ArrayAccess */ + public function getStrings(): array | ArrayAccess; +} + +function myFunction(MyInterface $container): string { + $strings = $container->getStrings(); + assertType('array|ArrayAccess', $strings); + assertType('string|null', $strings['test']); + return $strings['test']; +} + +function myOtherFunction(MyInterface $container): string { + $strings = $container->getStrings(); + assertType('array|ArrayAccess', $strings); + if (isset($strings['test'])) { + assertType('string', $strings['test']); + return $strings['test']; + } else { + throw new Exception(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12126.php b/tests/PHPStan/Analyser/nsrt/bug-12126.php new file mode 100644 index 0000000000..c494d8d60d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12126.php @@ -0,0 +1,36 @@ += 7.4 + +namespace Bug12126; + +use function PHPStan\Testing\assertType; + + +class HelloWorld +{ + public function sayHello(): void + { + $options = ['footest', 'testfoo']; + $key = array_rand($options, 1); + + $regex = '/foo(?Ptest)|test(?Pfoo)/J'; + if (!preg_match_all($regex, $options[$key], $matches, PREG_SET_ORDER)) { + return; + } + + assertType('list>', $matches); + // could be assertType("list", $matches); + if (!preg_match_all($regex, $options[$key], $matches, PREG_PATTERN_ORDER)) { + return; + } + + assertType('array>', $matches); + // could be assertType("array{0: list, test: list<'foo'|'test'>, 1: list<'test'|''>, 2: list<''|'foo'>}", $matches); + + if (!preg_match($regex, $options[$key], $matches)) { + return; + } + + assertType('array', $matches); + // could be assertType("array{0: list, test: 'foo', 1: '', 2: 'foo'}|array{0: list, test: 'test', 1: 'test', 2: ''}", $matches); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1216.php b/tests/PHPStan/Analyser/nsrt/bug-1216.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-1216.php rename to tests/PHPStan/Analyser/nsrt/bug-1216.php index 1ccb7d093d..7c0beae95d 100644 --- a/tests/PHPStan/Analyser/data/bug-1216.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1216.php @@ -2,6 +2,7 @@ namespace Bug1216; +use AllowDynamicProperties; use function PHPStan\Testing\assertType; abstract class Foo @@ -27,6 +28,7 @@ trait Bar * @property string $bar * @property string $untypedBar */ +#[AllowDynamicProperties] class Baz extends Foo { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12173.php b/tests/PHPStan/Analyser/nsrt/bug-12173.php new file mode 100644 index 0000000000..e92ce7da4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12173.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug12173; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function parse(string $string): void + { + $regex = '#.*(?(apple|orange)).*#'; + + if (preg_match($regex, $string, $matches) !== 1) { + throw new \Exception('Invalid input'); + } + + assertType("'apple'|'orange'", $matches['fruit']);; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12182.php b/tests/PHPStan/Analyser/nsrt/bug-12182.php new file mode 100644 index 0000000000..5566a2a2da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12182.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug12182; + +use ArrayObject; +use function PHPStan\Testing\assertType; + +/** + * @extends ArrayObject + */ +class HelloWorld extends ArrayObject +{ + public function __construct(private int $a = 42) { + } +} + +function (HelloWorld $hw): void { + assertType('array', (array) $hw); +}; diff --git a/tests/PHPStan/Analyser/data/bug-1219.php b/tests/PHPStan/Analyser/nsrt/bug-1219.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1219.php rename to tests/PHPStan/Analyser/nsrt/bug-1219.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php new file mode 100644 index 0000000000..13cf62ed26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -0,0 +1,27 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12210; + +use function PHPStan\Testing\assertType; + +function bug12210a(string $text): void { + assert(preg_match('(((sum|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); +} + +function bug12210b(string $text): void { + assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210c(string $text): void { + assert(preg_match('(((su.|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210d(string $text): void { + assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php new file mode 100644 index 0000000000..33131edfe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -0,0 +1,16 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12211; + +use function PHPStan\Testing\assertType; + +const REGEX = '((m.x))'; + +function foo(string $text): void { + assert(preg_match(REGEX, $text, $match) === 1); + assertType('array{non-falsy-string, non-falsy-string}', $match); +} + + diff --git a/tests/PHPStan/Analyser/nsrt/bug-12225.php b/tests/PHPStan/Analyser/nsrt/bug-12225.php new file mode 100644 index 0000000000..83647d0bff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12225.php @@ -0,0 +1,44 @@ += 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..b17b11aed8 --- /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('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, int>', $list); + } + assertType('array, int>', $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, int>', $list); + } + assertType('array, int>', $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-12376.php b/tests/PHPStan/Analyser/nsrt/bug-12376.php new file mode 100644 index 0000000000..a37fe35b91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12376.php @@ -0,0 +1,66 @@ + $class + * @return T + */ +function newNonFinalInstance(string $class): object +{ + return new $class(); +} + +class Base {} + +class A extends Base +{ + /** + * @template T of object + * @param T $object + * @return (T is static ? T : static) + * + * @phpstan-assert static $object + */ + public static function assertInstanceOf(object $object) + { + if (!$object instanceof static) { + throw new \Exception(); + } + + return $object; + } +} + +class B extends A {} +class C extends Base {} + +$o = newNonFinalInstance(\DateTime::class); +$r = A::assertInstanceOf($o); +assertType('*NEVER*', $o); +assertType('Bug12376\A', $r); + +$o = newNonFinalInstance(A::class); +$r = A::assertInstanceOf($o); +assertType('Bug12376\A', $o); +assertType('Bug12376\A', $r); + +$o = newNonFinalInstance(B::class); +$r = A::assertInstanceOf($o); +assertType('Bug12376\B', $o); +assertType('Bug12376\B', $r); + +$o = newNonFinalInstance(C::class); +$r = A::assertInstanceOf($o); +assertType('*NEVER*', $o); +assertType('Bug12376\A', $r); + +$o = newNonFinalInstance(A::class); +$r = B::assertInstanceOf($o); +assertType('Bug12376\B', $o); +assertType('Bug12376\B', $r); 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..a21dcf561a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -0,0 +1,708 @@ += 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', $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-12447-bis.php b/tests/PHPStan/Analyser/nsrt/bug-12447-bis.php new file mode 100644 index 0000000000..14ba281334 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12447-bis.php @@ -0,0 +1,22 @@ +doFoo(); + assertType('*ERROR*', $a); + $a[] = 5; + assertType('mixed', $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12447.php b/tests/PHPStan/Analyser/nsrt/bug-12447.php new file mode 100644 index 0000000000..21f35c89e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12447.php @@ -0,0 +1,15 @@ += 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-12828.php b/tests/PHPStan/Analyser/nsrt/bug-12828.php new file mode 100644 index 0000000000..db3437cc5e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12828.php @@ -0,0 +1,10 @@ + 'def', 'hello' => 'world']; +assertType("array{abc: 'def', hello: 'world'}", $a); +$a = array_replace($a, ['hello' => 'country']); +assertType("array{abc: 'def', hello: 'country'}", $a); diff --git a/tests/PHPStan/Analyser/data/bug-1283.php b/tests/PHPStan/Analyser/nsrt/bug-1283.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-1283.php rename to tests/PHPStan/Analyser/nsrt/bug-1283.php index fb1e5a2e0e..058c739bb9 100644 --- a/tests/PHPStan/Analyser/data/bug-1283.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1283.php @@ -21,7 +21,7 @@ function (array $levels): void { throw new \UnexpectedValueException(sprintf('Unsupported level `%s`', $level)); } - assertType('array(0 => 1, ?1 => 3)', $allowedElements); + assertType('array{0: 1, 1?: 3}', $allowedElements); assertVariableCertainty(TrinaryLogic::createYes(), $allowedElements); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12834.php b/tests/PHPStan/Analyser/nsrt/bug-12834.php new file mode 100644 index 0000000000..de44fa47f8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12834.php @@ -0,0 +1,16 @@ +test()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12866.php b/tests/PHPStan/Analyser/nsrt/bug-12866.php new file mode 100644 index 0000000000..4360d8bdd1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12866.php @@ -0,0 +1,65 @@ += 8.0 + +namespace Bug12866; + +use function PHPStan\Testing\assertType; + +interface I +{ + /** + * @phpstan-assert-if-true A $this + */ + public function isA(): bool; +} + +class A implements I +{ + public function isA(): bool + { + return true; + } +} + +class B implements I +{ + public function isA(): bool + { + return false; + } +} + +function takesI(I $i): void +{ + if (!$i->isA()) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesIStrictComparison(I $i): void +{ + if ($i->isA() !== true) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesNullableI(?I $i): void +{ + if (!$i?->isA()) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesNullableIStrictComparison(?I $i): void +{ + if ($i?->isA() !== true) { + return; + } + + assertType('Bug12866\\A', $i); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12891.php b/tests/PHPStan/Analyser/nsrt/bug-12891.php new file mode 100644 index 0000000000..a932a97491 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12891.php @@ -0,0 +1,22 @@ + */ + private iterable $builders; + + /** + * @param iterable $builders + */ + public function __construct(iterable $builders) { + $this->builders = $builders; + assertType('iterable<(int|string), string>', $builders); + assertType('iterable<(int|string), string>', $this->builders); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12894.php b/tests/PHPStan/Analyser/nsrt/bug-12894.php new file mode 100644 index 0000000000..67efdf1947 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12894.php @@ -0,0 +1,53 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12894; + +use Closure; + +/** + * @template TValue of object|null + */ +interface Dependency +{ + + /** + * @return TValue + */ + public function __invoke(): object|null; + +} + +interface DependencyResolver +{ + + /** + * @template V of object|null + * @template D of Dependency + * + * @param D $dependency + * + * @return V + */ + public function resolve(Dependency $dependency): object|null; + +} + +class Resolver implements DependencyResolver +{ + /** + * @var Closure(object|null): void + */ + protected Closure $run; + + public function resolve(Dependency $dependency): object|null { + $resolved = $dependency(); + \PHPStan\Testing\assertType('V of object|null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved); + $result = is_object($resolved) ? 1 : 2; + \PHPStan\Testing\assertType('V of object (method Bug12894\DependencyResolver::resolve(), argument)|V of null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved); + ($this->run)($resolved); + return $resolved; + } + +} 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-12933.php b/tests/PHPStan/Analyser/nsrt/bug-12933.php new file mode 100644 index 0000000000..bc57d3c57d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12933.php @@ -0,0 +1,48 @@ += 8.0 + +namespace Bug12933; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-type record array{id: positive-int, name: string} + */ +class Collection +{ + /** @param list $list */ + public function __construct( + public array $list + ) + { + } + + public function updateNameIsset(int $index, string $name): void + { + assert(isset($this->list[$index])); + assertType('int<0, max>', $index); + } + + public function updateNameArrayKeyExists(int $index, string $name): void + { + assert(array_key_exists($index, $this->list)); + assertType('int<0, max>', $index); + } + + /** + * @param int<-5, 5> $index + */ + public function issetNarrowsIntRange(int $index, string $name): void + { + assert(isset($this->list[$index])); + assertType('int<0, 5>', $index); + } + + /** + * @param int<5, 15> $index + */ + public function issetNotWidensIntRange(int $index, string $name): void + { + assert(isset($this->list[$index])); + assertType('int<5, 15>', $index); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12954.php b/tests/PHPStan/Analyser/nsrt/bug-12954.php new file mode 100644 index 0000000000..5fbc508799 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12954.php @@ -0,0 +1,38 @@ + [ + 'name' => 'ROLE_USER', + 'description' => 'User role' + ], + 28 => [ + 'name' => 'ROLE_ADMIN', + 'description' => 'Admin role' + ], + 43 => [ + 'name' => 'ROLE_SUPER_ADMIN', + 'description' => 'SUPER Admin role' + ], +]; + +$list = ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN']; + +$result = array_column($plop, 'name', null); + +/** + * @param list $array + */ +function doSomething(array $array): void +{ + assertType('list', $array); +} + +doSomething($result); +doSomething($list); + +assertType('array{\'ROLE_USER\', \'ROLE_ADMIN\', \'ROLE_SUPER_ADMIN\'}', $result); +assertType('array{\'ROLE_USER\', \'ROLE_ADMIN\', \'ROLE_SUPER_ADMIN\'}', $list); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php new file mode 100644 index 0000000000..0b81de9238 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php @@ -0,0 +1,29 @@ += 8.4 + +namespace Bug12973Php84; + +use function PHPStan\Testing\assertType; + +function mbtrim($value): void +{ + if (mb_trim($value) === '') { + assertType('mixed', $value); + } else { + assertType('mixed', $value); + } + assertType('mixed', $value); + + if (mb_ltrim($value) === '') { + assertType('mixed', $value); + } else { + assertType('mixed', $value); + } + assertType('mixed', $value); + + if (mb_rtrim($value) === '') { + assertType('mixed', $value); + } else { + assertType('mixed', $value); + } + assertType('mixed', $value); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12973.php b/tests/PHPStan/Analyser/nsrt/bug-12973.php new file mode 100644 index 0000000000..f1a916ad5a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12973.php @@ -0,0 +1,79 @@ +> */ + $transactions = []; + + assertType('list>', $transactions); + + foreach (array_keys($transactions) as $k) { + $transactions[$k]['Shares'] = []; + $transactions[$k]['Shares']['Projects'] = []; + $transactions[$k]['Shares']['People'] = []; + } + + assertType('list>', $transactions); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-13069.php b/tests/PHPStan/Analyser/nsrt/bug-13069.php new file mode 100644 index 0000000000..3e0a3f1271 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13069.php @@ -0,0 +1,31 @@ +getName(); + break; + case Account::class: + assertType(Account::class, $object); + echo $object->getMail(); + break; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13076.php b/tests/PHPStan/Analyser/nsrt/bug-13076.php new file mode 100644 index 0000000000..b91cdeb5b7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13076.php @@ -0,0 +1,26 @@ +hasAttributes()) { + assertType('DOMNamedNodeMap', $node->attributes); + } else { + assertType('DOMNamedNodeMap|null', $node->attributes); + } + } + + public function testElement(\DOMElement $node): void + { + if ($node->hasAttributes()) { + assertType('DOMNamedNodeMap', $node->attributes); + } else { + assertType('DOMNamedNodeMap', $node->attributes); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13088.php b/tests/PHPStan/Analyser/nsrt/bug-13088.php new file mode 100644 index 0000000000..2963034496 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13088.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug13088; + +use function PHPStan\dumpType; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(string $s, int $offset): void + { + if (preg_match('~msgstr "(.*)"\n~', $s, $matches, 0, $offset) === 1) { + assertType('array{non-falsy-string, string}', $matches); + } + } + + public function sayHello2(string $s, int $offset): void + { + if (preg_match('~msgstr "(.*)"\n~', $s, $matches, offset: $offset) === 1) { + assertType('array{non-falsy-string, string}', $matches); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13097.php b/tests/PHPStan/Analyser/nsrt/bug-13097.php new file mode 100644 index 0000000000..13acede3e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13097.php @@ -0,0 +1,14 @@ + 8]; } + +$b = foo(); +if (count($b) === 1) { + assertType('non-empty-array{0?: string, 1?: int}', $b); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13144.php b/tests/PHPStan/Analyser/nsrt/bug-13144.php new file mode 100644 index 0000000000..fb47f4d862 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13144.php @@ -0,0 +1,23 @@ + 1, 'b' => 2]); + +assertType('ArrayObject', $arr); // correctly inferred as `ArrayObject` + +$a = $arr['a']; // ok +$b = $arr['b']; // ok + +assertType('int|null', $a); // correctly inferred as `int|null` +assertType('int|null', $b); // correctly inferred as `int|null` + + +['a' => $a, 'b' => $b] = $arr; // ok + +assertType('int|null', $a); // incorrectly inferred as `mixed` +assertType('int|null', $b); // incorrectly inferred as `mixed` diff --git a/tests/PHPStan/Analyser/nsrt/bug-13197.php b/tests/PHPStan/Analyser/nsrt/bug-13197.php new file mode 100644 index 0000000000..c16af1da1c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13197.php @@ -0,0 +1,42 @@ +', $pipes); + + if (!is_resource($process)) { + return null; + } + + fclose($pipes[0]); + + $stdout = (string) stream_get_contents($pipes[1]); + $stderr = (string) stream_get_contents($pipes[2]); + + proc_close($process); + + return [$stdout, $stderr]; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13197b.php b/tests/PHPStan/Analyser/nsrt/bug-13197b.php new file mode 100644 index 0000000000..9067928453 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13197b.php @@ -0,0 +1,26 @@ + ['pipe', 'wb'], // https://stackoverflow.com/questions/28909347/is-it-possible-to-connect-more-than-the-two-standard-streams-to-a-terminal-in-li#28909376 + 5 => ['pipe', 'wb'], + ], + $pipes + ); + + assertType('array<0|3|5, resource>', $pipes); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13214.php b/tests/PHPStan/Analyser/nsrt/bug-13214.php new file mode 100644 index 0000000000..3145f1d4b5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13214.php @@ -0,0 +1,41 @@ + $array + */ + public function sayHello(ArrayAccess $array): void + { + $child = new stdClass(); + + assert($array[1] === null); + + assertType('null', $array[1]); + + $array[1] = $child; + + assertType(stdClass::class, $array[1]); + } + + /** + * @param array $array + */ + public function sayHelloArray(array $array): void + { + $child = new stdClass(); + + assert(($array[1] ?? null) === null); + + assertType('object|null', $array[1]); + + $array[1] = $child; + + assertType(stdClass::class, $array[1]); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270a.php b/tests/PHPStan/Analyser/nsrt/bug-13270a.php new file mode 100644 index 0000000000..428771dbff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13270a.php @@ -0,0 +1,61 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13270a; + +use function PHPStan\Testing\assertType; + +final class HelloWorld +{ + /** + * @param array $data + */ + public function test(array $data): void + { + foreach($data as $k => $v) { + assertType('non-empty-array', $data); + $data[$k]['a'] = true; + assertType("non-empty-array", $data); + foreach($data[$k] as $val) { + } + } + } + + public function doFoo( + mixed $mixed, + mixed $mixed2, + mixed $mixed3, + mixed $mixed4, + int $i, + int $i2, + string|int $stringOrInt + ): void + { + $mixed[$i]['a'] = true; + assertType('mixed', $mixed); + + $mixed2[$stringOrInt]['a'] = true; + assertType('mixed', $mixed2); + + $mixed3[$i][$stringOrInt] = true; + assertType('mixed', $mixed3); + + $mixed4['a'][$stringOrInt] = true; + assertType('mixed', $mixed4); + + $null = null; + $null[$i]['a'] = true; + assertType('non-empty-array', $null); + + $i2['a'] = true; + assertType('*ERROR*', $i2); + } + + public function mixedIntoForeach(mixed $m): void + { + foreach ($m as $k => $v) { + assertType('mixed~array{}', $m); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270b.php b/tests/PHPStan/Analyser/nsrt/bug-13270b.php new file mode 100644 index 0000000000..a921ed1ddb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b.php @@ -0,0 +1,28 @@ + $a + * @param non-empty-array $b + */ + public function test($a, $b) + { + assertType('string', current($a)($b)); + } +} + +class Bar +{ + /** @return string */ + public function __invoke($b) + { + return ''; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13312.php b/tests/PHPStan/Analyser/nsrt/bug-13312.php new file mode 100644 index 0000000000..3a7ab22873 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13312.php @@ -0,0 +1,45 @@ += 8.0 + +namespace Bug13312; + +use function PHPStan\Testing\assertType; + +function fooArr(array $arr): void { + assertType('array', $arr); + foreach ($arr as $v) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); + + for ($i = 0; $i < count($arr); ++$i) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); +} + +/** @param list $arr */ +function foo(array $arr): void { + assertType('list', $arr); + foreach ($arr as $v) { + assertType('non-empty-list', $arr); + } + assertType('list', $arr); + + for ($i = 0; $i < count($arr); ++$i) { + assertType('non-empty-list', $arr); + } + assertType('list', $arr); +} + + +function fooBar(mixed $mixed): void { + assertType('mixed', $mixed); + foreach ($mixed as $v) { + assertType('mixed~array{}', $mixed); + } + assertType('mixed', $mixed); + + foreach ($mixed as $v) {} + + assertType('mixed', $mixed); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13321.php b/tests/PHPStan/Analyser/nsrt/bug-13321.php new file mode 100644 index 0000000000..66a1f724d5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13321.php @@ -0,0 +1,65 @@ += 8.1 + +namespace Bug13321; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function __construct(public readonly string $value) + { + } +} + +class Bar +{ + public function __construct( + private readonly ?Foo $foo, + private ?Foo $writableFoo = null, + ) + { + } + + public function bar(): void + { + (function () { + assertType(Foo::class.'|null', $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + })(); + + if ($this->foo === null) { + return; + } + if ($this->writableFoo === null) { + return; + } + + (function () { + assertType(Foo::class, $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + })(); + + $test = function () { + assertType(Foo::class, $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + }; + + $test(); + + $test = static function () { + assertType('mixed', $this->foo); + assertType('mixed', $this->writableFoo); + + echo $this->foo->value; + }; + + $test(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13321b.php b/tests/PHPStan/Analyser/nsrt/bug-13321b.php new file mode 100644 index 0000000000..27e6a25fe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13321b.php @@ -0,0 +1,50 @@ += 8.2 + +namespace Bug13321b; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function __construct( + public string $value, + readonly public string $readonlyValue, + ) + { + } +} + +readonly class Bar +{ + public function __construct( + private ?Foo $foo, + ) + { + } + + public function bar(): void + { + if ($this->foo === null) { + return; + } + if ($this->foo->value === '') { + return; + } + if ($this->foo->readonlyValue === '') { + return; + } + + assertType(Foo::class, $this->foo); + assertType('non-empty-string', $this->foo->value); + assertType('non-empty-string', $this->foo->readonlyValue); + + $test = function () { + assertType(Foo::class, $this->foo); + assertType('string', $this->foo->value); + assertType('non-empty-string', $this->foo->readonlyValue); + }; + + $test(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13342.php b/tests/PHPStan/Analyser/nsrt/bug-13342.php new file mode 100644 index 0000000000..de6d80dd61 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13342.php @@ -0,0 +1,22 @@ +test); + assertType('bool', $crate->test); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13365.php b/tests/PHPStan/Analyser/nsrt/bug-13365.php new file mode 100644 index 0000000000..df8c9cfef1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13365.php @@ -0,0 +1,29 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13365; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function test(\DOMElement $element): void + { + $attributes = $element->attributes; + + if ($attributes === null) { + return; + } + + assertType('DOMNamedNodeMap', $attributes); + assertType('Iterator', $attributes->getIterator()); + assertType('DOMAttr|null', $attributes->getNamedItem('foo')); + assertType('DOMAttr|null', $attributes->getNamedItemNS('foo', 'bar')); + assertType('DOMAttr|null', $attributes->item(0)); + + foreach ($element->attributes ?? [] as $attr) { + assertType('DOMAttr', $attr); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13377.php b/tests/PHPStan/Analyser/nsrt/bug-13377.php new file mode 100644 index 0000000000..03f6c8e14d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13377.php @@ -0,0 +1,44 @@ +', $possiblyEmptyInvalid); + + $possiblyEmptyInvalid = array_keys($array, 'yes', true); + assertType('list<(int|string)>', $possiblyEmptyInvalid); + + $possiblyEmptyValid = array_keys(array_filter($array, fn($val) => $val == 'yes')); + assertType('list<(int|string)>', $possiblyEmptyValid); +} + + + +/** + * @return array + */ +function random_array(): array +{ + $return = []; + for($i = 0; $i < 10; $i++){ + if(random_int(0, 1) === 1){ + $return[] = 'yes'; + }else{ + $return[] = 'no'; + } + } + + return $return; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13385.php b/tests/PHPStan/Analyser/nsrt/bug-13385.php new file mode 100644 index 0000000000..c8502eaaa5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13385.php @@ -0,0 +1,51 @@ + $children + */ + public function calculate(array $children): int { + $operands = []; + $operators = []; + + foreach ($children as $child) { + if ($child instanceof Operator) { + while ($operators !== [] && end($operators)->priority() >= $child->priority()) { + $op = array_pop($operators); + $left = array_pop($operands) ?? 0; + $right = array_pop($operands) ?? 0; + + assert(is_int($left)); + assert(is_int($right)); + + $value = $op->calculate($left, $right); + + assertType(Operator::class, $op); + assertType('int', $left); + assertType('int', $right); + assertType('int', $value); + + $operands[] = $value; + + assertType('non-empty-list', $operands); + } + + $operators[] = $child; + } else { + $operands[] = $child; + } + } + + return count($operands) === 1 ? reset($operands) : 0; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13385b.php b/tests/PHPStan/Analyser/nsrt/bug-13385b.php new file mode 100644 index 0000000000..64864b11ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13385b.php @@ -0,0 +1,51 @@ + $children + */ + public function calculate(array $children): int { + $operands = []; + $operators = []; + + foreach ($children as $child) { + if ($child instanceof Operator) { + while ($operators !== []) { + $op = array_pop($operators); + $left = array_pop($operands) ?? 0; + $right = array_pop($operands) ?? 0; + + assert(is_int($left)); + assert(is_int($right)); + + $value = $op->calculate($left, $right); + + assertType(Operator::class, $op); + assertType('int', $left); + assertType('int', $right); + assertType('int', $value); + + $operands[] = $value; + + assertType('non-empty-list', $operands); + } + + $operators[] = $child; + } else { + $operands[] = $child; + } + } + + return count($operands) === 1 ? reset($operands) : 0; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13392.php b/tests/PHPStan/Analyser/nsrt/bug-13392.php new file mode 100644 index 0000000000..6637a89832 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13392.php @@ -0,0 +1,21 @@ +get(); + assertType('RedisCluster',$redisCluster); + + $transaction = $redisCluster->multi(); + assertType('(bool|RedisCluster)',$transaction); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13411.php b/tests/PHPStan/Analyser/nsrt/bug-13411.php new file mode 100644 index 0000000000..60e39d1d18 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13411.php @@ -0,0 +1,17 @@ + */ +class Tokens extends SplFixedArray {} + +/** @param array|Tokens $tokens */ +function x(iterable $tokens): int { + assertType('array|Bug13411\\Tokens', $tokens); + return count($tokens); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13450.php b/tests/PHPStan/Analyser/nsrt/bug-13450.php new file mode 100644 index 0000000000..f69cb39300 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13450.php @@ -0,0 +1,74 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13450; + +use function PHPStan\Testing\assertType; + +/** + * @template TRelated of Model + * @template TDeclaring of Model + * @template TResult + */ +abstract class Relation +{ + /** @return TResult */ + public function getResults(): mixed + { + return []; // @phpstan-ignore return.type + } +} + +/** + * @template TRelated of Model + * @template TDeclaring of Model + * @template TPivot of Pivot = Pivot + * @template TAccessor of string = 'pivot' + * + * @extends Relation> + */ +class BelongsToMany extends Relation {} + +abstract class Model +{ + /** + * @template TRelated of Model + * @param class-string $related + * @return BelongsToMany + */ + public function belongsToMany(string $related): BelongsToMany + { + return new BelongsToMany(); // @phpstan-ignore return.type + } + + public function __get(string $name): mixed { return null; } + public function __set(string $name, mixed $value): void {} +} + +class Pivot extends Model {} + +class User extends Model +{ + /** @return BelongsToMany */ + public function teams(): BelongsToMany + { + return $this->belongsToMany(Team::class); + } + + /** @return BelongsToMany */ + public function teamsFinal(): BelongsToMany + { + return $this->belongsToMany(TeamFinal::class); + } +} + +class Team extends Model {} + +final class TeamFinal extends Model {} + +function test(User $user): void +{ + assertType('array', $user->teams()->getResults()); + assertType('array', $user->teamsFinal()->getResults()); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13481.php b/tests/PHPStan/Analyser/nsrt/bug-13481.php new file mode 100644 index 0000000000..bd3eee12cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13481.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug13552; + +use function PHPStan\Testing\assertType; + +function doSomething(mixed $value): void +{ + if (trim($value) === '') { + return; + } + assertType('mixed', $value); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1452.php b/tests/PHPStan/Analyser/nsrt/bug-1452.php new file mode 100644 index 0000000000..c4c40325c6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1452.php @@ -0,0 +1,10 @@ +diff(new \DateTimeImmutable('now')); + +// Could be lowercase-string&non-falsy-string&numeric-string&uppercase-string +assertType('lowercase-string&non-falsy-string', $dateInterval->format('%a')); diff --git a/tests/PHPStan/Analyser/data/bug-1511.php b/tests/PHPStan/Analyser/nsrt/bug-1511.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1511.php rename to tests/PHPStan/Analyser/nsrt/bug-1511.php diff --git a/tests/PHPStan/Analyser/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/data/bug-1597.php b/tests/PHPStan/Analyser/nsrt/bug-1597.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-1597.php rename to tests/PHPStan/Analyser/nsrt/bug-1597.php index 5023defbb8..def66f078e 100644 --- a/tests/PHPStan/Analyser/data/bug-1597.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1597.php @@ -7,6 +7,9 @@ $date = ''; try { + if (rand(0,1) === 0) { + throw new \Exception(); + } $date = new \DateTime($date); } catch (\Exception $e) { assertType('\'\'', $date); diff --git a/tests/PHPStan/Analyser/data/bug-1657.php b/tests/PHPStan/Analyser/nsrt/bug-1657.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1657.php rename to tests/PHPStan/Analyser/nsrt/bug-1657.php diff --git a/tests/PHPStan/Analyser/data/bug-1670.php b/tests/PHPStan/Analyser/nsrt/bug-1670.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1670.php rename to tests/PHPStan/Analyser/nsrt/bug-1670.php diff --git a/tests/PHPStan/Analyser/data/bug-1801.php b/tests/PHPStan/Analyser/nsrt/bug-1801.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1801.php rename to tests/PHPStan/Analyser/nsrt/bug-1801.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-1861.php b/tests/PHPStan/Analyser/nsrt/bug-1861.php new file mode 100644 index 0000000000..4d5335a67c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1861.php @@ -0,0 +1,29 @@ +children)) { + case 0: + assertType('array{}', $this->children); + break; + case 1: + assertType('non-empty-array<' . self::class . '>', $this->children); + assertType(self::class, reset($this->children)); + break; + default: + assertType('non-empty-array<' . self::class . '>', $this->children); + break; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1865.php b/tests/PHPStan/Analyser/nsrt/bug-1865.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1865.php rename to tests/PHPStan/Analyser/nsrt/bug-1865.php diff --git a/tests/PHPStan/Analyser/data/bug-1870.php b/tests/PHPStan/Analyser/nsrt/bug-1870.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1870.php rename to tests/PHPStan/Analyser/nsrt/bug-1870.php diff --git a/tests/PHPStan/Analyser/data/bug-1897.php b/tests/PHPStan/Analyser/nsrt/bug-1897.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1897.php rename to tests/PHPStan/Analyser/nsrt/bug-1897.php diff --git a/tests/PHPStan/Analyser/data/bug-1924.php b/tests/PHPStan/Analyser/nsrt/bug-1924.php similarity index 83% rename from tests/PHPStan/Analyser/data/bug-1924.php rename to tests/PHPStan/Analyser/nsrt/bug-1924.php index 048ccd3ff6..05c13dab62 100644 --- a/tests/PHPStan/Analyser/data/bug-1924.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1924.php @@ -18,7 +18,7 @@ function foo(): void 'a' => $this->getArrayOrNull(), 'b' => $this->getArrayOrNull(), ]; - assertType('array(\'a\' => array|null, \'b\' => array|null)', $arr); + assertType('array{a: array|null, b: array|null}', $arr); $cond = isset($arr['a']) && isset($arr['b']); assertType('bool', $cond); diff --git a/tests/PHPStan/Analyser/data/bug-1945.php b/tests/PHPStan/Analyser/nsrt/bug-1945.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1945.php rename to tests/PHPStan/Analyser/nsrt/bug-1945.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001.php b/tests/PHPStan/Analyser/nsrt/bug-2001.php new file mode 100644 index 0000000000..69d429d8bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -0,0 +1,51 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); + + if (array_key_exists('host', $parsedUrl)) { + assertType('array{scheme?: string, host: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); + throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.'); + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); + + $redirectUrl = $parsedUrl['path']; + + if (array_key_exists('query', $parsedUrl)) { + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $parsedUrl); + $redirectUrl .= '?' . $parsedUrl['query']; + } + + if (array_key_exists('fragment', $parsedUrl)) { + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl); + $redirectUrl .= '#' . $parsedUrl['query']; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); + + return $redirectUrl; + } + + public function doFoo(int $i) + { + $a = ['a' => $i]; + if (rand(0, 1)) { + $a['b'] = $i; + } + + if (rand(0,1)) { + $a = ['d' => $i]; + } + + assertType('array{a: int, b?: int}|array{d: int}', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2003.php b/tests/PHPStan/Analyser/nsrt/bug-2003.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2003.php rename to tests/PHPStan/Analyser/nsrt/bug-2003.php diff --git a/tests/PHPStan/Analyser/data/bug-2112.php b/tests/PHPStan/Analyser/nsrt/bug-2112.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-2112.php rename to tests/PHPStan/Analyser/nsrt/bug-2112.php index 0a4a0a5bde..65634c415b 100644 --- a/tests/PHPStan/Analyser/data/bug-2112.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2112.php @@ -19,7 +19,7 @@ public function doBar(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('array&nonEmpty', $foos); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); } /** @return self[] */ @@ -35,7 +35,7 @@ public function doBars(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('array&nonEmpty', $foos); + 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/data/bug-2231.php b/tests/PHPStan/Analyser/nsrt/bug-2231.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2231.php rename to tests/PHPStan/Analyser/nsrt/bug-2231.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2232.php b/tests/PHPStan/Analyser/nsrt/bug-2232.php new file mode 100644 index 0000000000..7464edf141 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2232.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/data/bug-2288.php b/tests/PHPStan/Analyser/nsrt/bug-2288.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2288.php rename to tests/PHPStan/Analyser/nsrt/bug-2288.php diff --git a/tests/PHPStan/Analyser/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/data/bug-2413.php b/tests/PHPStan/Analyser/nsrt/bug-2413.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2413.php rename to tests/PHPStan/Analyser/nsrt/bug-2413.php diff --git a/tests/PHPStan/Analyser/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 100% rename from tests/PHPStan/Analyser/data/bug-2443.php rename to tests/PHPStan/Analyser/nsrt/bug-2443.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2471.php b/tests/PHPStan/Analyser/nsrt/bug-2471.php new file mode 100644 index 0000000000..6b405cfb66 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2471.php @@ -0,0 +1,28 @@ +doFoo(); + + $x = array_fill_keys($y, null); + + assertType('array', $x); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2539.php b/tests/PHPStan/Analyser/nsrt/bug-2539.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2539.php rename to tests/PHPStan/Analyser/nsrt/bug-2539.php diff --git a/tests/PHPStan/Analyser/data/bug-2549.php b/tests/PHPStan/Analyser/nsrt/bug-2549.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2549.php rename to tests/PHPStan/Analyser/nsrt/bug-2549.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2580.php b/tests/PHPStan/Analyser/nsrt/bug-2580.php new file mode 100644 index 0000000000..98d5a8160c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2580.php @@ -0,0 +1,16 @@ + $typeName + * @param mixed $value + */ +function cast($value, string $typeName): void { + if (is_object($value) && get_class($value) === $typeName) { + assertType('T of object (function Bug2580\cast(), argument)', $value); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php new file mode 100644 index 0000000000..bf13358857 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php @@ -0,0 +1,26 @@ + 7.4 + +namespace Bug2600PhpVersionScope; + +use function PHPStan\Testing\assertType; + +if (PHP_VERSION_ID >= 80000) { + class Foo8 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + } +} else { + class Foo9 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + } + +} diff --git a/tests/PHPStan/Analyser/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/data/bug-2600.php b/tests/PHPStan/Analyser/nsrt/bug-2600.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-2600.php rename to tests/PHPStan/Analyser/nsrt/bug-2600.php index 07ca71fd77..9ab5e49598 100644 --- a/tests/PHPStan/Analyser/data/bug-2600.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2600.php @@ -1,4 +1,4 @@ -', $args); } /** @@ -26,20 +26,20 @@ public function doBar($x = null) { * @param mixed $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param mixed ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } public function doIpsum($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } } @@ -51,7 +51,7 @@ class Bar public function doFoo($x = null) { $args = func_get_args(); assertType('string|null', $x); - assertType('array', $args); + assertType('list', $args); } /** @@ -65,24 +65,24 @@ public function doBar($x = null) { * @param string $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param string ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } } function foo($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); } function ($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); }; diff --git a/tests/PHPStan/Analyser/data/bug-2611.php b/tests/PHPStan/Analyser/nsrt/bug-2611.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2611.php rename to tests/PHPStan/Analyser/nsrt/bug-2611.php diff --git a/tests/PHPStan/Analyser/data/bug-2612.php b/tests/PHPStan/Analyser/nsrt/bug-2612.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2612.php rename to tests/PHPStan/Analyser/nsrt/bug-2612.php diff --git a/tests/PHPStan/Analyser/data/bug-2640.php b/tests/PHPStan/Analyser/nsrt/bug-2640.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2640.php rename to tests/PHPStan/Analyser/nsrt/bug-2640.php diff --git a/tests/PHPStan/Analyser/data/bug-2648.php b/tests/PHPStan/Analyser/nsrt/bug-2648.php similarity index 89% rename from tests/PHPStan/Analyser/data/bug-2648.php rename to tests/PHPStan/Analyser/nsrt/bug-2648.php index 23797170a1..9acaa05026 100644 --- a/tests/PHPStan/Analyser/data/bug-2648.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2648.php @@ -15,7 +15,7 @@ public function doFoo(array $list): void if (count($list) > 1) { assertType('int<2, max>', count($list)); unset($list['fooo']); - assertType('array', $list); + assertType("array", $list); assertType('int<0, max>', count($list)); } } @@ -37,6 +37,8 @@ public function doBar(array $list): void assertType('int<0, max>', count($list)); if (count($list) === 1) { + assertType('1', count($list)); + $list[] = false; assertType('int<1, max>', count($list)); break; } diff --git a/tests/PHPStan/Analyser/data/bug-2676.php b/tests/PHPStan/Analyser/nsrt/bug-2676.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-2676.php rename to tests/PHPStan/Analyser/nsrt/bug-2676.php index 4daa2b5552..30723db8a9 100644 --- a/tests/PHPStan/Analyser/data/bug-2676.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2676.php @@ -39,7 +39,7 @@ function (Wallet $wallet): void assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); foreach ($bankAccounts as $key => $bankAccount) { - assertType('(int|string)', $key); + assertType('mixed', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Analyser/data/bug-2677.php b/tests/PHPStan/Analyser/nsrt/bug-2677.php similarity index 88% rename from tests/PHPStan/Analyser/data/bug-2677.php rename to tests/PHPStan/Analyser/nsrt/bug-2677.php index c191dd1f3d..22656ae4a3 100644 --- a/tests/PHPStan/Analyser/data/bug-2677.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2677.php @@ -41,7 +41,7 @@ function () O::class, P::class, ]; - assertType('array(\'Bug2677\\\\A\', \'Bug2677\\\\B\', \'Bug2677\\\\C\', \'Bug2677\\\\D\', \'Bug2677\\\\E\', \'Bug2677\\\\F\', \'Bug2677\\\\G\', \'Bug2677\\\\H\', \'Bug2677\\\\I\', \'Bug2677\\\\J\', \'Bug2677\\\\K\', \'Bug2677\\\\L\', \'Bug2677\\\\M\', \'Bug2677\\\\N\', \'Bug2677\\\\O\', \'Bug2677\\\\P\')', $classes); + assertType('array{\'Bug2677\\\\A\', \'Bug2677\\\\B\', \'Bug2677\\\\C\', \'Bug2677\\\\D\', \'Bug2677\\\\E\', \'Bug2677\\\\F\', \'Bug2677\\\\G\', \'Bug2677\\\\H\', \'Bug2677\\\\I\', \'Bug2677\\\\J\', \'Bug2677\\\\K\', \'Bug2677\\\\L\', \'Bug2677\\\\M\', \'Bug2677\\\\N\', \'Bug2677\\\\O\', \'Bug2677\\\\P\'}', $classes); foreach ($classes as $class) { assertType('\'Bug2677\\\\A\'|\'Bug2677\\\\B\'|\'Bug2677\\\\C\'|\'Bug2677\\\\D\'|\'Bug2677\\\\E\'|\'Bug2677\\\\F\'|\'Bug2677\\\\G\'|\'Bug2677\\\\H\'|\'Bug2677\\\\I\'|\'Bug2677\\\\J\'|\'Bug2677\\\\K\'|\'Bug2677\\\\L\'|\'Bug2677\\\\M\'|\'Bug2677\\\\N\'|\'Bug2677\\\\O\'|\'Bug2677\\\\P\'', $class); diff --git a/tests/PHPStan/Analyser/nsrt/bug-2718.php b/tests/PHPStan/Analyser/nsrt/bug-2718.php new file mode 100644 index 0000000000..cbd4640939 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2718.php @@ -0,0 +1,41 @@ + 'a', 'user_id' => 'id1'], + ['group_id' => 'a', 'user_id' => 'id2'], + ['group_id' => 'a', 'user_id' => 'id3'], + ['group_id' => 'b', 'user_id' => 'id4'], + ['group_id' => 'b', 'user_id' => 'id5'], + ['group_id' => 'b', 'user_id' => 'id6'], + ]; + }; + + $orders = $fun(); + + $result = []; + foreach ($orders as $order) { + assertType('bool', isset($result[$order['group_id']]['users'])); + if (isset($result[$order['group_id']]['users'])) { + $result[$order['group_id']]['users'][] = $order['user_id']; + continue; + } + + $result[$order['group_id']] = [ + 'users' => [ + $order['user_id'], + ], + ]; + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-2733.php b/tests/PHPStan/Analyser/nsrt/bug-2733.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-2733.php rename to tests/PHPStan/Analyser/nsrt/bug-2733.php index d40acdc032..f18f5053d9 100644 --- a/tests/PHPStan/Analyser/data/bug-2733.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2733.php @@ -18,7 +18,7 @@ public function doSomething(array $data): void } } - assertType('array(\'id\' => int, \'name\' => string)', $data); + assertType('array{id: int, name: string}', $data); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2735.php b/tests/PHPStan/Analyser/nsrt/bug-2735.php new file mode 100644 index 0000000000..a486eda5c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2735.php @@ -0,0 +1,134 @@ + */ + protected $arr = []; + + /** + * @param array $arr + */ + public function __construct(array $arr) { + $this->arr = $arr; + } + + /** + * @return T + */ + public function last() + { + if (!$this->arr) { + throw new \Exception('bad'); + } + return end($this->arr); + } +} + +/** + * @template T + * @extends Collection + */ +class CollectionChild extends Collection { +} + +$dogs = new CollectionChild([new Dog(), new Dog()]); +assertType('Bug2735\\CollectionChild', $dogs); + +/** + * @template X + * @template Y + */ +class ParentWithConstructor +{ + + /** + * @param X $x + * @param Y $y + */ + public function __construct($x, $y) + { + } + +} + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildOne extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildOne(1, new Dog()); + assertType('Bug2735\\ChildOne', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildTwo extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildTwo(new Cat(), 2); + assertType('Bug2735\\ChildTwo', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildThree extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildThree(new Cat(), new Dog()); + assertType('Bug2735\\ChildThree', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFour extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFour(new Cat(), new Dog()); + assertType('Bug2735\\ChildFour', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFive extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFive(new Cat(), new Dog()); + assertType('Bug2735\\ChildFive', $a); +}; diff --git a/tests/PHPStan/Analyser/data/bug-2740.php b/tests/PHPStan/Analyser/nsrt/bug-2740.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2740.php rename to tests/PHPStan/Analyser/nsrt/bug-2740.php diff --git a/tests/PHPStan/Analyser/data/bug-2750.php b/tests/PHPStan/Analyser/nsrt/bug-2750.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2750.php rename to tests/PHPStan/Analyser/nsrt/bug-2750.php diff --git a/tests/PHPStan/Analyser/data/bug-2760.php b/tests/PHPStan/Analyser/nsrt/bug-2760.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2760.php rename to tests/PHPStan/Analyser/nsrt/bug-2760.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2806.php b/tests/PHPStan/Analyser/nsrt/bug-2806.php new file mode 100644 index 0000000000..a34e1233c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2806.php @@ -0,0 +1,15 @@ +', $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 100% rename from tests/PHPStan/Analyser/data/bug-2850.php rename to tests/PHPStan/Analyser/nsrt/bug-2850.php diff --git a/tests/PHPStan/Analyser/data/bug-2863.php b/tests/PHPStan/Analyser/nsrt/bug-2863.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-2863.php rename to tests/PHPStan/Analyser/nsrt/bug-2863.php index 5f70c4795a..1e81b90d1d 100644 --- a/tests/PHPStan/Analyser/data/bug-2863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2863.php @@ -5,7 +5,7 @@ use function PHPStan\Testing\assertType; $result = json_decode('{"a":5}'); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -17,7 +17,7 @@ // $result2 = json_decode(''); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -29,7 +29,7 @@ // $result3 = json_encode([]); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { diff --git a/tests/PHPStan/Analyser/data/bug-2869.php b/tests/PHPStan/Analyser/nsrt/bug-2869.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2869.php rename to tests/PHPStan/Analyser/nsrt/bug-2869.php diff --git a/tests/PHPStan/Analyser/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/data/bug-2927.php b/tests/PHPStan/Analyser/nsrt/bug-2927.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2927.php rename to tests/PHPStan/Analyser/nsrt/bug-2927.php diff --git a/tests/PHPStan/Analyser/data/bug-2945.php b/tests/PHPStan/Analyser/nsrt/bug-2945.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2945.php rename to tests/PHPStan/Analyser/nsrt/bug-2945.php diff --git a/tests/PHPStan/Analyser/data/bug-2969.php b/tests/PHPStan/Analyser/nsrt/bug-2969.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2969.php rename to tests/PHPStan/Analyser/nsrt/bug-2969.php diff --git a/tests/PHPStan/Analyser/data/bug-2977.php b/tests/PHPStan/Analyser/nsrt/bug-2977.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2977.php rename to tests/PHPStan/Analyser/nsrt/bug-2977.php diff --git a/tests/PHPStan/Analyser/data/bug-2980.php b/tests/PHPStan/Analyser/nsrt/bug-2980.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2980.php rename to tests/PHPStan/Analyser/nsrt/bug-2980.php diff --git a/tests/PHPStan/Analyser/data/bug-2997.php b/tests/PHPStan/Analyser/nsrt/bug-2997.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2997.php rename to tests/PHPStan/Analyser/nsrt/bug-2997.php diff --git a/tests/PHPStan/Analyser/data/bug-3004.php b/tests/PHPStan/Analyser/nsrt/bug-3004.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3004.php rename to tests/PHPStan/Analyser/nsrt/bug-3004.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3009.php b/tests/PHPStan/Analyser/nsrt/bug-3009.php new file mode 100644 index 0000000000..969efdc372 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3009.php @@ -0,0 +1,30 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $redirectUrlParts); + return null; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + + if (true === array_key_exists('query', $redirectUrlParts)) { + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $redirectUrlParts); + $redirectServer['QUERY_STRING'] = $redirectUrlParts['query']; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + + return 'foo'; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3013.php b/tests/PHPStan/Analyser/nsrt/bug-3013.php new file mode 100644 index 0000000000..7039b43910 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3013.php @@ -0,0 +1,59 @@ +', $foo); + + $bar = $this->intOrNull(); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true)) { + assertType('non-empty-array', $foo); + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true) === true) { + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + } + + + public function intOrNull(): ?int + { + return rand() === 2 ? null : rand(); + } + + /** + * @param array{0: 1, 1?: 2} $foo + */ + public function testArrayKeyExists($foo): void + { + assertType("array{0: 1, 1?: 2}", $foo); + + $bar = 1; + assertType("1", $bar); + + if (array_key_exists($bar, $foo) === true) { + assertType("array{1, 2}", $foo); + assertType("1", $bar); + return; + } + + assertType("array{1}", $foo); + assertType("1", $bar); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3019.php b/tests/PHPStan/Analyser/nsrt/bug-3019.php new file mode 100644 index 0000000000..1ea4949c3f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3019.php @@ -0,0 +1,35 @@ +> + */ + private function getIntArrayFloatArray(): array + { + return [ + 0 => [1.1, 2.2, 3.3], + 1 => [1.1, 2.2, 3.3], + 2 => [1.1, 2.2, 3.3], + ]; + } + + /** + * @return array> + */ + public function invalidType(): void + { + $X = $this->getIntArrayFloatArray(); + $L = $this->getIntArrayFloatArray(); + + $n = 3; + $m = 3; + + for ($k = 0; $k < $m; ++$k) { + for ($j = 0; $j < $n; ++$j) { + for ($i = 0; $i < $k; ++$i) { + $X[$k][$j] -= $X[$i][$j] * $L[$k][$i]; + } + } + } + + assertType('array>', $X); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3106.php b/tests/PHPStan/Analyser/nsrt/bug-3106.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3106.php rename to tests/PHPStan/Analyser/nsrt/bug-3106.php 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/data/bug-3132.php b/tests/PHPStan/Analyser/nsrt/bug-3132.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3132.php rename to tests/PHPStan/Analyser/nsrt/bug-3132.php 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/data/bug-3134.php b/tests/PHPStan/Analyser/nsrt/bug-3134.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3134.php rename to tests/PHPStan/Analyser/nsrt/bug-3134.php diff --git a/tests/PHPStan/Analyser/data/bug-3142.php b/tests/PHPStan/Analyser/nsrt/bug-3142.php similarity index 96% rename from tests/PHPStan/Analyser/data/bug-3142.php rename to tests/PHPStan/Analyser/nsrt/bug-3142.php index e46fd7fd22..b1a53ee3cc 100644 --- a/tests/PHPStan/Analyser/data/bug-3142.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3142.php @@ -35,7 +35,7 @@ public function sayHello() function (): void { $hw = new HelloWorld(); assertType('string', $hw->sayHi()); - assertType('int', $hw->sayHello()); + assertType('string', $hw->sayHello()); }; interface DecoratorInterface diff --git a/tests/PHPStan/Analyser/data/bug-3158.php b/tests/PHPStan/Analyser/nsrt/bug-3158.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3158.php rename to tests/PHPStan/Analyser/nsrt/bug-3158.php diff --git a/tests/PHPStan/Analyser/data/bug-3190.php b/tests/PHPStan/Analyser/nsrt/bug-3190.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3190.php rename to tests/PHPStan/Analyser/nsrt/bug-3190.php diff --git a/tests/PHPStan/Analyser/data/bug-3226.php b/tests/PHPStan/Analyser/nsrt/bug-3226.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3226.php rename to tests/PHPStan/Analyser/nsrt/bug-3226.php diff --git a/tests/PHPStan/Analyser/data/bug-3266.php b/tests/PHPStan/Analyser/nsrt/bug-3266.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-3266.php rename to tests/PHPStan/Analyser/nsrt/bug-3266.php index 9e1d2a3ebe..bee0c7e6f8 100644 --- a/tests/PHPStan/Analyser/data/bug-3266.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3266.php @@ -21,7 +21,7 @@ public function iteratorToArray($iterator) assertType('TKey of (int|string) (method Bug3266\Foo::iteratorToArray(), argument)', $key); assertType('TValue (method Bug3266\Foo::iteratorToArray(), argument)', $value); $array[$key] = $value; - assertType('array&nonEmpty', $array); + assertType('non-empty-array', $array); } assertType('array', $array); diff --git a/tests/PHPStan/Analyser/data/bug-3269.php b/tests/PHPStan/Analyser/nsrt/bug-3269.php similarity index 77% rename from tests/PHPStan/Analyser/data/bug-3269.php rename to tests/PHPStan/Analyser/nsrt/bug-3269.php index b3cfdbea68..4a0d7fef71 100644 --- a/tests/PHPStan/Analyser/data/bug-3269.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3269.php @@ -20,10 +20,10 @@ public static function bar(array $intervalGroups): void } } - assertType('array string, \'operator\' => string, \'side\' => \'end\'|\'start\')>', $borders); + assertType("list", $borders); foreach ($borders as $border) { - assertType('array(\'version\' => string, \'operator\' => string, \'side\' => \'end\'|\'start\')', $border); + assertType("array{version: string, operator: string, side: 'end'|'start'}", $border); assertType('\'end\'|\'start\'', $border['side']); } } diff --git a/tests/PHPStan/Analyser/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/data/bug-3321.php b/tests/PHPStan/Analyser/nsrt/bug-3321.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3321.php rename to tests/PHPStan/Analyser/nsrt/bug-3321.php diff --git a/tests/PHPStan/Analyser/data/bug-3331.php b/tests/PHPStan/Analyser/nsrt/bug-3331.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3331.php rename to tests/PHPStan/Analyser/nsrt/bug-3331.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3336.php b/tests/PHPStan/Analyser/nsrt/bug-3336.php new file mode 100644 index 0000000000..b0707c61aa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3336.php @@ -0,0 +1,10 @@ +combine($a, $b); - assertType('array|false', $c); + assertType("array<'a'|'b'|'c', 1|2|3>|false", $c); - assertType('array(\'a\' => 1, \'b\' => 2, \'c\' => 3)', array_combine($a, $b)); + assertType('array{a: 1, b: 2, c: 3}', array_combine($a, $b)); } /** diff --git a/tests/PHPStan/Analyser/data/bug-3379.php b/tests/PHPStan/Analyser/nsrt/bug-3379.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-3379.php rename to tests/PHPStan/Analyser/nsrt/bug-3379.php index 53d82890e3..4500775f5a 100644 --- a/tests/PHPStan/Analyser/data/bug-3379.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3379.php @@ -13,5 +13,5 @@ class Foo function () { echo Foo::URL; - assertType('mixed', Foo::URL); + assertType('non-falsy-string', Foo::URL); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3382.php b/tests/PHPStan/Analyser/nsrt/bug-3382.php new file mode 100644 index 0000000000..65899b7d27 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3382.php @@ -0,0 +1,9 @@ +', $var); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3474.php b/tests/PHPStan/Analyser/nsrt/bug-3474.php new file mode 100644 index 0000000000..db53e9bbec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3474.php @@ -0,0 +1,26 @@ +', $a['test']); + assertType('array', $a["test"]); + + acceptsArray($a['test']); +} diff --git a/tests/PHPStan/Analyser/data/bug-3548.php b/tests/PHPStan/Analyser/nsrt/bug-3548.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3548.php rename to tests/PHPStan/Analyser/nsrt/bug-3548.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3558.php b/tests/PHPStan/Analyser/nsrt/bug-3558.php new file mode 100644 index 0000000000..2f71cf07d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3558.php @@ -0,0 +1,33 @@ + 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/data/bug-3617.php b/tests/PHPStan/Analyser/nsrt/bug-3617.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3617.php rename to tests/PHPStan/Analyser/nsrt/bug-3617.php diff --git a/tests/PHPStan/Analyser/data/bug-3677.php b/tests/PHPStan/Analyser/nsrt/bug-3677.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-3677.php rename to tests/PHPStan/Analyser/nsrt/bug-3677.php index 0280d14773..e01f685ec3 100644 --- a/tests/PHPStan/Analyser/data/bug-3677.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3677.php @@ -61,8 +61,7 @@ public function sayGoodbye(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ?: $second); + assertType(Field::class, $first ?: $second); assertType(Field::class, $first ?? $second); } } @@ -71,8 +70,7 @@ public function sayGoodbye2(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ? $first : $second); + assertType(Field::class, $first ? $first : $second); assertType(Field::class, $first ?? $second); } } diff --git a/tests/PHPStan/Analyser/data/bug-3710.php b/tests/PHPStan/Analyser/nsrt/bug-3710.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3710.php rename to tests/PHPStan/Analyser/nsrt/bug-3710.php diff --git a/tests/PHPStan/Analyser/data/bug-3760.php b/tests/PHPStan/Analyser/nsrt/bug-3760.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3760.php rename to tests/PHPStan/Analyser/nsrt/bug-3760.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3789.php b/tests/PHPStan/Analyser/nsrt/bug-3789.php new file mode 100644 index 0000000000..95934512e3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3789.php @@ -0,0 +1,23 @@ +} $params + * @return ($params is array{wrapperClass:mixed} ? T : Connection) + */ + public static function getConnection(array $params): Connection { + return new Connection(); + } + + public static function test(): void { + assertType('Bug3853\Connection', DriverManager::getConnection([])); + assertType('Bug3853\SubConnection', DriverManager::getConnection(['wrapperClass' => SubConnection::class])); + } +} diff --git a/tests/PHPStan/Analyser/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 @@ +&nonEmpty', $lengths); + assertType('non-empty-list', $lengths); } public static function getInt(): int diff --git a/tests/PHPStan/Analyser/data/bug-3922.php b/tests/PHPStan/Analyser/nsrt/bug-3922.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3922.php rename to tests/PHPStan/Analyser/nsrt/bug-3922.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php new file mode 100644 index 0000000000..9eaeff4a72 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Bug3961Php8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(string $v, string $d, $m): void + { + assertType('non-empty-list', explode('.', $v)); + assertType('*NEVER*', explode('', $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('non-empty-list', explode($d, $v)); + assertType('non-empty-list', explode($m, $v)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961.php b/tests/PHPStan/Analyser/nsrt/bug-3961.php new file mode 100644 index 0000000000..b4725ec070 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3961.php @@ -0,0 +1,21 @@ +', explode('.', $v)); + assertType('false', explode('', $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('non-empty-list|false', explode($d, $v)); + assertType('(non-empty-list|false)', explode($m, $v)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3981.php b/tests/PHPStan/Analyser/nsrt/bug-3981.php new file mode 100644 index 0000000000..c962983a3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3981.php @@ -0,0 +1,54 @@ +getType() which is `stdClass|array|null` here + assertNativeType('array{}|null', $config); + assertType('array{}|null', $config); + $config = new \stdClass(); + } elseif (! (is_array($config) || $config instanceof \stdClass)) { + assertNativeType('mixed~(0|0.0|\'\'|\'0\'|array{}|stdClass|false|null)', $config); + assertType('*NEVER*', $config); + } + + return new \stdClass($config); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3993.php b/tests/PHPStan/Analyser/nsrt/bug-3993.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-3993.php rename to tests/PHPStan/Analyser/nsrt/bug-3993.php index 4c106dbab8..a1a24e380d 100644 --- a/tests/PHPStan/Analyser/data/bug-3993.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3993.php @@ -13,11 +13,11 @@ public function doFoo($arguments) return; } - assertType('mixed~null', $arguments); + assertType('mixed~(array{}|null)', $arguments); array_shift($arguments); - assertType('mixed~null', $arguments); + assertType('array', $arguments); assertType('int<0, max>', count($arguments)); } diff --git a/tests/PHPStan/Analyser/data/bug-3997.php b/tests/PHPStan/Analyser/nsrt/bug-3997.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3997.php rename to tests/PHPStan/Analyser/nsrt/bug-3997.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4016.php b/tests/PHPStan/Analyser/nsrt/bug-4016.php new file mode 100644 index 0000000000..c6d67ad772 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4016.php @@ -0,0 +1,36 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + $a[] = 2; + assertType('non-empty-array', $a); + + unset($a[0]); + assertType('array|int<1, max>, int>', $a); + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + $a[1] = 2; + assertType('non-empty-array&hasOffsetValue(1, 2)', $a); + + unset($a[1]); + assertType('array|int<2, max>, int>', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4091.php b/tests/PHPStan/Analyser/nsrt/bug-4091.php new file mode 100644 index 0000000000..ba691e03b1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4091.php @@ -0,0 +1,10 @@ + 3) { + echo 'Fizz'; + assertType('int<0, 10>', mt_rand(0,10)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099.php b/tests/PHPStan/Analyser/nsrt/bug-4099.php new file mode 100644 index 0000000000..5e5eb30ca2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4099.php @@ -0,0 +1,41 @@ + + */ +class GenericList implements IteratorAggregate +{ + /** @var array */ + protected $items = []; + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + /** + * @return ?T + */ + public function broken(int $key) + { + $item = $this->items[$key] ?? null; + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + if ($item) { + assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); + } else { + assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); + } + + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + + return $item; + } + + /** + * @return ?T + */ + public function works(int $key) + { + $item = $this->items[$key] ?? null; + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + + return $item; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4177.php b/tests/PHPStan/Analyser/nsrt/bug-4177.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4177.php rename to tests/PHPStan/Analyser/nsrt/bug-4177.php diff --git a/tests/PHPStan/Analyser/data/bug-4188.php b/tests/PHPStan/Analyser/nsrt/bug-4188.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-4188.php rename to tests/PHPStan/Analyser/nsrt/bug-4188.php index 07a44f9458..a34cca26a4 100644 --- a/tests/PHPStan/Analyser/data/bug-4188.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4188.php @@ -1,6 +1,6 @@ -= 7.4 +', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } @@ -30,7 +30,7 @@ public function setShort(array $data): void $data, fn($param): bool => $param instanceof B, ); - assertType('array', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } diff --git a/tests/PHPStan/Analyser/data/bug-4190.php b/tests/PHPStan/Analyser/nsrt/bug-4190.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4190.php rename to tests/PHPStan/Analyser/nsrt/bug-4190.php diff --git a/tests/PHPStan/Analyser/data/bug-4205.php b/tests/PHPStan/Analyser/nsrt/bug-4205.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4205.php rename to tests/PHPStan/Analyser/nsrt/bug-4205.php diff --git a/tests/PHPStan/Analyser/data/bug-4206.php b/tests/PHPStan/Analyser/nsrt/bug-4206.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4206.php rename to tests/PHPStan/Analyser/nsrt/bug-4206.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4207.php b/tests/PHPStan/Analyser/nsrt/bug-4207.php new file mode 100644 index 0000000000..756eef4c9f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4207.php @@ -0,0 +1,10 @@ +>', range(1, 10000)); + assertType('non-empty-list>', range(10000, 1)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-4209-2.php b/tests/PHPStan/Analyser/nsrt/bug-4209-2.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4209-2.php rename to tests/PHPStan/Analyser/nsrt/bug-4209-2.php diff --git a/tests/PHPStan/Analyser/data/bug-4209.php b/tests/PHPStan/Analyser/nsrt/bug-4209.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4209.php rename to tests/PHPStan/Analyser/nsrt/bug-4209.php diff --git a/tests/PHPStan/Analyser/data/bug-4213.php b/tests/PHPStan/Analyser/nsrt/bug-4213.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4213.php rename to tests/PHPStan/Analyser/nsrt/bug-4213.php index bc9742be9a..a8f06cb1c5 100644 --- a/tests/PHPStan/Analyser/data/bug-4213.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4213.php @@ -1,4 +1,4 @@ -= 8.1 namespace Bug4213; @@ -37,7 +37,7 @@ public function setEnumsWithoutSplat(array $enums): void { function (): void { assertType('Bug4213\Enum', Enum::get('test')); - assertType('array(Bug4213\Enum)', array_map([Enum::class, 'get'], ['test'])); + assertType('array{Bug4213\\Enum}', array_map([Enum::class, 'get'], ['test'])); }; diff --git a/tests/PHPStan/Analyser/data/bug-4215.php b/tests/PHPStan/Analyser/nsrt/bug-4215.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4215.php rename to tests/PHPStan/Analyser/nsrt/bug-4215.php diff --git a/tests/PHPStan/Analyser/data/bug-4231.php b/tests/PHPStan/Analyser/nsrt/bug-4231.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4231.php rename to tests/PHPStan/Analyser/nsrt/bug-4231.php diff --git a/tests/PHPStan/Analyser/data/bug-4247.php b/tests/PHPStan/Analyser/nsrt/bug-4247.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4247.php rename to tests/PHPStan/Analyser/nsrt/bug-4247.php diff --git a/tests/PHPStan/Analyser/data/bug-4267.php b/tests/PHPStan/Analyser/nsrt/bug-4267.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-4267.php rename to tests/PHPStan/Analyser/nsrt/bug-4267.php index be533bafcd..c93256827b 100644 --- a/tests/PHPStan/Analyser/data/bug-4267.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4267.php @@ -10,6 +10,7 @@ class Model1 implements \IteratorAggregate { + #[\ReturnTypeWillChange] public function getIterator(): iterable { throw new \Exception('not implemented'); @@ -33,6 +34,7 @@ class Model2 implements \IteratorAggregate /** * @return iterable */ + #[\ReturnTypeWillChange] public function getIterator(): iterable { throw new \Exception('not implemented'); diff --git a/tests/PHPStan/Analyser/data/bug-4287.php b/tests/PHPStan/Analyser/nsrt/bug-4287.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4287.php rename to tests/PHPStan/Analyser/nsrt/bug-4287.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4302b.php b/tests/PHPStan/Analyser/nsrt/bug-4302b.php new file mode 100644 index 0000000000..ff24b6a689 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4302b.php @@ -0,0 +1,20 @@ += 7.4 + */ + 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/data/bug-4415.php b/tests/PHPStan/Analyser/nsrt/bug-4415.php similarity index 75% rename from tests/PHPStan/Analyser/data/bug-4415.php rename to tests/PHPStan/Analyser/nsrt/bug-4415.php index 338401f86a..8dce55c08e 100644 --- a/tests/PHPStan/Analyser/data/bug-4415.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4415.php @@ -19,7 +19,7 @@ public function getIterator(): \Iterator function (Foo $foo): void { foreach ($foo as $k => $v) { - assertType('int', $k); - assertType('string', $v); + assertType('mixed', $k); // should be int + assertType('mixed', $v); // should be string } }; diff --git a/tests/PHPStan/Analyser/data/bug-4423.php b/tests/PHPStan/Analyser/nsrt/bug-4423.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4423.php rename to tests/PHPStan/Analyser/nsrt/bug-4423.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4434.php b/tests/PHPStan/Analyser/nsrt/bug-4434.php new file mode 100644 index 0000000000..7b48df20fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4434.php @@ -0,0 +1,42 @@ +', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + if (PHP_MAJOR_VERSION === 7) { + assertType('7', PHP_MAJOR_VERSION); + assertType('7', \PHP_MAJOR_VERSION); + } else { + assertType('8|int<5, 6>', PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', \PHP_MAJOR_VERSION); + } + } + } +} + +class HelloWorld2 +{ + public function testSendEmailToLog(): void + { + foreach ([1] as $emailFile) { + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + if (PHP_MAJOR_VERSION === 100) { + assertType('*NEVER*', PHP_MAJOR_VERSION); + assertType('*NEVER*', \PHP_MAJOR_VERSION); + } else { + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4436.php b/tests/PHPStan/Analyser/nsrt/bug-4436.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4436.php rename to tests/PHPStan/Analyser/nsrt/bug-4436.php diff --git a/tests/PHPStan/Analyser/data/bug-4498.php b/tests/PHPStan/Analyser/nsrt/bug-4498.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-4498.php rename to tests/PHPStan/Analyser/nsrt/bug-4498.php index 19e878c763..ad07baa3db 100644 --- a/tests/PHPStan/Analyser/data/bug-4498.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4498.php @@ -38,7 +38,7 @@ public function fcn(iterable $iterable): iterable public function bar(iterable $iterable): iterable { if (is_array($iterable)) { - assertType('array', $iterable); + assertType('array<((int&TKey (method Bug4498\Foo::bar(), argument))|(string&TKey (method Bug4498\Foo::bar(), argument))), TValue (method Bug4498\Foo::bar(), argument)>', $iterable); return $iterable; } diff --git a/tests/PHPStan/Analyser/data/bug-4499.php b/tests/PHPStan/Analyser/nsrt/bug-4499.php similarity index 82% rename from tests/PHPStan/Analyser/data/bug-4499.php rename to tests/PHPStan/Analyser/nsrt/bug-4499.php index 5c4b78f63f..fe65958259 100644 --- a/tests/PHPStan/Analyser/data/bug-4499.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4499.php @@ -11,7 +11,7 @@ class Foo function thing(array $things) : void{ switch(count($things)){ case 1: - assertType('array&nonEmpty', $things); + assertType('array{int}', $things); assertType('int', array_shift($things)); } } diff --git a/tests/PHPStan/Analyser/data/bug-4500.php b/tests/PHPStan/Analyser/nsrt/bug-4500.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4500.php rename to tests/PHPStan/Analyser/nsrt/bug-4500.php diff --git a/tests/PHPStan/Analyser/data/bug-4504.php b/tests/PHPStan/Analyser/nsrt/bug-4504.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-4504.php rename to tests/PHPStan/Analyser/nsrt/bug-4504.php index 50ba802671..ceab5de4e2 100644 --- a/tests/PHPStan/Analyser/data/bug-4504.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4504.php @@ -14,7 +14,7 @@ public function sayHello($models): void assertType('Bug4504TypeInference\A', $v); } - assertType('array()|Iterator', $models); + assertType('Iterator', $models); } } diff --git a/tests/PHPStan/Analyser/data/bug-4538.php b/tests/PHPStan/Analyser/nsrt/bug-4538.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-4538.php rename to tests/PHPStan/Analyser/nsrt/bug-4538.php index ebdbec8e48..20be659998 100644 --- a/tests/PHPStan/Analyser/data/bug-4538.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4538.php @@ -12,6 +12,6 @@ class Foo public function bar(string $index): void { assertType('string|false', getenv($index)); - assertType('array', getenv()); + assertType('array', getenv()); } } diff --git a/tests/PHPStan/Analyser/data/bug-4545.php b/tests/PHPStan/Analyser/nsrt/bug-4545.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4545.php rename to tests/PHPStan/Analyser/nsrt/bug-4545.php index a7162e9f79..e7f48619cd 100644 --- a/tests/PHPStan/Analyser/data/bug-4545.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4545.php @@ -33,7 +33,7 @@ function compareMaps(Map $firstMap, Map $secondMap, Closure $comparator): Set foreach ($intersect as $key) { assertType('TValue1 (method Bug4545\Foo::compareMaps(), argument)', $firstMap->get($key)); assertType('TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key)); - assertType('int|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); + assertType('1|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); } return $keys; diff --git a/tests/PHPStan/Analyser/data/bug-4557.php b/tests/PHPStan/Analyser/nsrt/bug-4557.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4557.php rename to tests/PHPStan/Analyser/nsrt/bug-4557.php diff --git a/tests/PHPStan/Analyser/data/bug-4558.php b/tests/PHPStan/Analyser/nsrt/bug-4558.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4558.php rename to tests/PHPStan/Analyser/nsrt/bug-4558.php index 89b250cc0b..a40e33581a 100644 --- a/tests/PHPStan/Analyser/data/bug-4558.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4558.php @@ -15,7 +15,7 @@ class HelloWorld public function sayHello(): ?DateTime { while (count($this->suggestions) > 0) { - assertType('array&nonEmpty', $this->suggestions); + assertType('non-empty-array', $this->suggestions); assertType('int<1, max>', count($this->suggestions)); $try = array_shift($this->suggestions); @@ -31,7 +31,7 @@ public function sayHello(): ?DateTime // we might be out of suggested days, so load some more if (count($this->suggestions) === 0) { - assertType('array()', $this->suggestions); + assertType('array{}', $this->suggestions); assertType('0', count($this->suggestions)); $this->createSuggestions(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4565.php b/tests/PHPStan/Analyser/nsrt/bug-4565.php new file mode 100644 index 0000000000..48ab02dd92 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4565.php @@ -0,0 +1,19 @@ + ''] + $variables['attributes']; + assertType('non-empty-array', $attributes); + if (!empty($variables['button'])) { + assertType('non-empty-array', $attributes); + $attributes['type'] = 'button'; + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); + unset($attributes['href']); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); + } + assertType('non-empty-array', $attributes); + return $attributes; +} diff --git a/tests/PHPStan/Analyser/data/bug-4573.php b/tests/PHPStan/Analyser/nsrt/bug-4573.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4573.php rename to tests/PHPStan/Analyser/nsrt/bug-4573.php diff --git a/tests/PHPStan/Analyser/data/bug-4577.php b/tests/PHPStan/Analyser/nsrt/bug-4577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4577.php rename to tests/PHPStan/Analyser/nsrt/bug-4577.php diff --git a/tests/PHPStan/Analyser/data/bug-4579.php b/tests/PHPStan/Analyser/nsrt/bug-4579.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4579.php rename to tests/PHPStan/Analyser/nsrt/bug-4579.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4586.php b/tests/PHPStan/Analyser/nsrt/bug-4586.php new file mode 100644 index 0000000000..82291ec0df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4586.php @@ -0,0 +1,13 @@ + $results */ + $results = []; + + $type = array_map(static function (array $result): array { + assertType('array{a: int}', $result); + return $result; + }, $results); + + assertType('list', $type); + } + + public function b(): void + { + /** @var list $results */ + $results = []; + + $type = array_map(static function (array $result): array { + assertType('array{a: int}', $result); + $result['a'] = (string) $result['a']; + assertType('array{a: lowercase-string&numeric-string&uppercase-string}', $result); + + return $result; + }, $results); + + assertType('list', $type); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4588.php b/tests/PHPStan/Analyser/nsrt/bug-4588.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4588.php rename to tests/PHPStan/Analyser/nsrt/bug-4588.php diff --git a/tests/PHPStan/Analyser/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/data/bug-4602.php b/tests/PHPStan/Analyser/nsrt/bug-4602.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4602.php rename to tests/PHPStan/Analyser/nsrt/bug-4602.php diff --git a/tests/PHPStan/Analyser/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/data/bug-4642.php b/tests/PHPStan/Analyser/nsrt/bug-4642.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4642.php rename to tests/PHPStan/Analyser/nsrt/bug-4642.php diff --git a/tests/PHPStan/Analyser/data/bug-4650.php b/tests/PHPStan/Analyser/nsrt/bug-4650.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-4650.php rename to tests/PHPStan/Analyser/nsrt/bug-4650.php index f378375869..f51b260c26 100644 --- a/tests/PHPStan/Analyser/data/bug-4650.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4650.php @@ -12,11 +12,11 @@ class Foo * @phpstan-param non-empty-array $idx */ function doFoo(array $idx): void { - assertType('array&nonEmpty', $idx); + assertType('non-empty-array', $idx); assertNativeType('array', $idx); - assertType('array()', []); - assertNativeType('array()', []); + assertType('array{}', []); + assertNativeType('array{}', []); assertType('false', $idx === []); assertNativeType('bool', $idx === []); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4657.php b/tests/PHPStan/Analyser/nsrt/bug-4657.php new file mode 100644 index 0000000000..db175d8a6d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4657.php @@ -0,0 +1,58 @@ +', $count); + + $a = []; + if (isset($array['a'])) $a[] = $array['a']; + if (isset($array['b'])) $a[] = $array['b']; + if (isset($array['c'])) $a[] = $array['c']; + if (isset($array['d'])) $a[] = $array['d']; + if (isset($array['e'])) $a[] = $array['e']; + if (count($a) >= $count) { + assertType('int<1, 5>', count($a)); + assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + } else { + assertType('0', count($a)); + assertType('array{}', $a); + } +}; + +function(array $array, int $count): void { + if ($count < 1) { + return; + } + + assertType('int<1, max>', $count); + + $a = []; + if (isset($array['a'])) $a[] = $array['a']; + if (isset($array['b'])) $a[] = $array['b']; + if (isset($array['c'])) $a[] = $array['c']; + if (isset($array['d'])) $a[] = $array['d']; + if (isset($array['e'])) $a[] = $array['e']; + if (count($a) > $count) { + assertType('int<2, 5>', count($a)); + assertType('list{0: mixed~null, 1: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + } else { + assertType('int<0, 5>', count($a)); // Could be int<0, 1> + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-4707.php b/tests/PHPStan/Analyser/nsrt/bug-4707.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4707.php rename to tests/PHPStan/Analyser/nsrt/bug-4707.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4708.php b/tests/PHPStan/Analyser/nsrt/bug-4708.php new file mode 100644 index 0000000000..b6f2302722 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4708.php @@ -0,0 +1,96 @@ + FALSE, + 'dberror' => 'xyz']; + } + else + { + assertType('array', $result); + if (!isset($result['bsw'])) + { + assertType('array', $result); + $result['bsw'] = 1; + assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bsw', string)", $result); + $result['bsw'] = (int) $result['bsw']; + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + + if (!isset($result['bew'])) + { + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + $result['bew'] = 5; + assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bew', int|string)&hasOffsetValue('bsw', int)", $result); + $result['bew'] = (int) $result['bew']; + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + + foreach (['utc', 'ssi'] as $field) + { + if (array_key_exists($field, $result)) + { + $result[$field] = (int) $result[$field]; + } + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType('non-empty-array', $result); + + return $result; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php new file mode 100644 index 0000000000..9050c74c15 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4711.php @@ -0,0 +1,19 @@ +', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4714.php b/tests/PHPStan/Analyser/nsrt/bug-4714.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4714.php rename to tests/PHPStan/Analyser/nsrt/bug-4714.php diff --git a/tests/PHPStan/Analyser/data/bug-4725.php b/tests/PHPStan/Analyser/nsrt/bug-4725.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4725.php rename to tests/PHPStan/Analyser/nsrt/bug-4725.php diff --git a/tests/PHPStan/Analyser/data/bug-4733.php b/tests/PHPStan/Analyser/nsrt/bug-4733.php similarity index 80% rename from tests/PHPStan/Analyser/data/bug-4733.php rename to tests/PHPStan/Analyser/nsrt/bug-4733.php index 39961cc464..dec6f9bd3b 100644 --- a/tests/PHPStan/Analyser/data/bug-4733.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4733.php @@ -23,6 +23,23 @@ public function getDescription(?\DateTimeImmutable $start, ?string $someObject): assertType('string', $someObject); } + public function getDescriptionn(?\DateTimeImmutable $start, ?string $someObject): void + { + if ($start !== null && $someObject !== null) { + return; + } + + // $start === null || $someObject === null + + if ($start === null) { + return; + } + + // $start !== null therefore $someObject === null + + assertType('null', $someObject); + } + public function getDescription2(?\DateTimeImmutable $start, ?string $someObject): void { if ($start !== null || $someObject !== null) { diff --git a/tests/PHPStan/Analyser/data/bug-4741.php b/tests/PHPStan/Analyser/nsrt/bug-4741.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4741.php rename to tests/PHPStan/Analyser/nsrt/bug-4741.php diff --git a/tests/PHPStan/Analyser/data/bug-4743.php b/tests/PHPStan/Analyser/nsrt/bug-4743.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4743.php rename to tests/PHPStan/Analyser/nsrt/bug-4743.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4754.php b/tests/PHPStan/Analyser/nsrt/bug-4754.php new file mode 100644 index 0000000000..ffa98ee0f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4754.php @@ -0,0 +1,42 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedComponentNotSpecified); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|int<0, 65535>|string|false|null', $parsedNotConstant); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedAllConstant); + assertType('string|false|null', $parsedSchemeConstant); + assertType('string|false|null', $parsedHostConstant); + assertType('int<0, 65535>|false|null', $parsedPortConstant); + assertType('string|false|null', $parsedUserConstant); + assertType('string|false|null', $parsedPassConstant); + assertType('string|false|null', $parsedPathConstant); + assertType('string|false|null', $parsedQueryConstant); + assertType('string|false|null', $parsedFragmentConstant); +} diff --git a/tests/PHPStan/Analyser/data/bug-4757.php b/tests/PHPStan/Analyser/nsrt/bug-4757.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4757.php rename to tests/PHPStan/Analyser/nsrt/bug-4757.php diff --git a/tests/PHPStan/Analyser/data/bug-4761.php b/tests/PHPStan/Analyser/nsrt/bug-4761.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4761.php rename to tests/PHPStan/Analyser/nsrt/bug-4761.php diff --git a/tests/PHPStan/Analyser/data/bug-4803.php b/tests/PHPStan/Analyser/nsrt/bug-4803.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4803.php rename to tests/PHPStan/Analyser/nsrt/bug-4803.php diff --git a/tests/PHPStan/Analyser/data/bug-4814.php b/tests/PHPStan/Analyser/nsrt/bug-4814.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-4814.php rename to tests/PHPStan/Analyser/nsrt/bug-4814.php index 4e8607a793..2fcaf231f4 100644 --- a/tests/PHPStan/Analyser/data/bug-4814.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4814.php @@ -33,7 +33,7 @@ public function doFoo() $decodedResponseBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $exception) { assertType('string|null', $body); - assertType('array()', $decodedResponseBody); + assertType('array{}', $decodedResponseBody); } } diff --git a/tests/PHPStan/Analyser/data/bug-4816.php b/tests/PHPStan/Analyser/nsrt/bug-4816.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4816.php rename to tests/PHPStan/Analyser/nsrt/bug-4816.php diff --git a/tests/PHPStan/Analyser/data/bug-4820.php b/tests/PHPStan/Analyser/nsrt/bug-4820.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4820.php rename to tests/PHPStan/Analyser/nsrt/bug-4820.php diff --git a/tests/PHPStan/Analyser/data/bug-4821.php b/tests/PHPStan/Analyser/nsrt/bug-4821.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4821.php rename to tests/PHPStan/Analyser/nsrt/bug-4821.php diff --git a/tests/PHPStan/Analyser/data/bug-4822.php b/tests/PHPStan/Analyser/nsrt/bug-4822.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4822.php rename to tests/PHPStan/Analyser/nsrt/bug-4822.php diff --git a/tests/PHPStan/Analyser/data/bug-4838.php b/tests/PHPStan/Analyser/nsrt/bug-4838.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4838.php rename to tests/PHPStan/Analyser/nsrt/bug-4838.php diff --git a/tests/PHPStan/Analyser/data/bug-4843.php b/tests/PHPStan/Analyser/nsrt/bug-4843.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4843.php rename to tests/PHPStan/Analyser/nsrt/bug-4843.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4875.php b/tests/PHPStan/Analyser/nsrt/bug-4875.php new file mode 100644 index 0000000000..ee0e89a44d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4875.php @@ -0,0 +1,37 @@ + $interface + * @return T&Mock + */ + function mockIt(string $interface): object + { + return eval("new class implements $interface, Mock {}"); + } + + function doFoo() + { + $mock = $this->mockIt(Blah::class); + + assertType('Bug4875\Blah&Bug4875\Mock', $mock); + assertType('class-string&literal-string', $mock::class); + assertType('class-string', get_class($mock)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4879.php b/tests/PHPStan/Analyser/nsrt/bug-4879.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4879.php rename to tests/PHPStan/Analyser/nsrt/bug-4879.php index 1c6c9536c4..26d74b1c73 100644 --- a/tests/PHPStan/Analyser/data/bug-4879.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4879.php @@ -33,7 +33,7 @@ public function sayHello2(bool $bool1): void $this->test(); } catch (\Exception $ex) { - assertVariableCertainty(TrinaryLogic::createNo(), $var); + assertVariableCertainty(TrinaryLogic::createMaybe(), $var); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4885.php b/tests/PHPStan/Analyser/nsrt/bug-4885.php new file mode 100644 index 0000000000..c047e0e41e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4885.php @@ -0,0 +1,22 @@ += 8.0 + +namespace Bug4885Types; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** @param array{word?: string} $data */ + public function sayHello(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + assertType('array{word: string}', $data); + } + + /** @param array{word?: string|null} $data */ + public function sayHi(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + assertType('array{word: string}', $data); + } +} diff --git a/tests/PHPStan/Analyser/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 @@ +>', $items); + $batch = array_splice($items, 0, 2); + assertType('list>', $items); + assertType('non-empty-list>', $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{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/data/bug-505.php b/tests/PHPStan/Analyser/nsrt/bug-505.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-505.php rename to tests/PHPStan/Analyser/nsrt/bug-505.php diff --git a/tests/PHPStan/Analyser/data/bug-5072.php b/tests/PHPStan/Analyser/nsrt/bug-5072.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-5072.php rename to tests/PHPStan/Analyser/nsrt/bug-5072.php index 0de9da5e91..25f273346b 100644 --- a/tests/PHPStan/Analyser/data/bug-5072.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5072.php @@ -24,6 +24,6 @@ public function incorrect(array $params): void public function incorrectWithConstant(): void { - assertType('int<1, max>', max(1, PHP_INT_MAX)); + 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/data/bug-5129.php b/tests/PHPStan/Analyser/nsrt/bug-5129.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5129.php rename to tests/PHPStan/Analyser/nsrt/bug-5129.php diff --git a/tests/PHPStan/Analyser/data/bug-5140.php b/tests/PHPStan/Analyser/nsrt/bug-5140.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5140.php rename to tests/PHPStan/Analyser/nsrt/bug-5140.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php new file mode 100644 index 0000000000..9e981de789 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug5168Php8; + +use function PHPStan\Testing\assertType; + +function (float $f): void { + define('LARAVEL_START', microtime(true)); + + $comment = 'Calculated in ' . microtime(true) - $f; + assertType('non-falsy-string', $comment); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5172.php b/tests/PHPStan/Analyser/nsrt/bug-5172.php new file mode 100644 index 0000000000..63519d8ca7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5172.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug5172; + +use function PHPStan\Testing\assertType; + +class Period +{ + public mixed $from; + public mixed $to; + + public function year(): ?int + { + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + // let's say $this->from === null && $model->to === null + + if ($this->from?->year !== $this->to?->year) { + return null; + } + + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + return $this->from?->year; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5219.php b/tests/PHPStan/Analyser/nsrt/bug-5219.php new file mode 100644 index 0000000000..f7431de04c --- /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/data/bug-5259.php b/tests/PHPStan/Analyser/nsrt/bug-5259.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5259.php rename to tests/PHPStan/Analyser/nsrt/bug-5259.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5262.php b/tests/PHPStan/Analyser/nsrt/bug-5262.php new file mode 100644 index 0000000000..229d9fbeb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5262.php @@ -0,0 +1,36 @@ + $testclass + * @return T + */ +function test(bool $optional = false, string $testclass = TestBase::class): TestBase +{ + return new $testclass(); +} + +class TestBase +{ +} + +class TestChild extends TestBase +{ + public function hello(): string + { + return 'world'; + } +} + +function runTest(): void +{ + assertType('Bug5262\TestChild', test(false, TestChild::class)); + assertType('Bug5262\TestChild', test(false, testclass: TestChild::class)); + assertType('Bug5262\TestChild', test(optional: false, testclass: TestChild::class)); + assertType('Bug5262\TestChild', test(testclass: TestChild::class, optional: false)); + assertType('Bug5262\TestChild', test(testclass: TestChild::class)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php new file mode 100644 index 0000000000..d1cb994c92 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php @@ -0,0 +1,61 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug5287Php81; + +use function PHPStan\Testing\assertType; + +/** + * @param list $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo5(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param array{foo: 17, bar: 19} $arr + */ +function bar(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array{foo: 17, bar: 19}', $arrSpread); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5287.php b/tests/PHPStan/Analyser/nsrt/bug-5287.php new file mode 100644 index 0000000000..83bbd544d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5287.php @@ -0,0 +1,61 @@ + $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo5(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param array{foo: 17, bar: 19} $arr + */ +function bar(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array{17, 19}', $arrSpread); +} diff --git a/tests/PHPStan/Analyser/data/bug-5293.php b/tests/PHPStan/Analyser/nsrt/bug-5293.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5293.php rename to tests/PHPStan/Analyser/nsrt/bug-5293.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5304.php b/tests/PHPStan/Analyser/nsrt/bug-5304.php new file mode 100644 index 0000000000..2346517d4d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5304.php @@ -0,0 +1,20 @@ + 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x > 0) { + assertType('1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function greaterEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x >= 0) { + assertType('0.0|1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '*NEVER*' when we support float-ranges + + return 1.0; +} + +function smaller(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x < 0) { + assertType('-1.0', $x); + return 5 / $x; + } + assertType('-1.0|0.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function smallerEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x <= 0) { + assertType('-1.0|0.0', $x); + return 5 / $x; + } + assertType('*NEVER*', $x); + + return 1.0; +} diff --git a/tests/PHPStan/Analyser/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/data/bug-5322.php b/tests/PHPStan/Analyser/nsrt/bug-5322.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5322.php rename to tests/PHPStan/Analyser/nsrt/bug-5322.php diff --git a/tests/PHPStan/Analyser/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 @@ + + */ + protected $items = []; + + /** + * @param array $items + * @return void + */ + public function __construct($items) + { + $this->items = $items; + } + + /** + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return self + */ + public function map(callable $callback) + { + $keys = array_keys($this->items); + + $items = array_map($callback, $this->items, $keys); + + return new self(array_combine($keys, $items)); + } + + /** + * @return array + */ + public function all() + { + return $this->items; + } +} + +function (): void { + $result = (new Collection(['book', 'cars']))->map(function($category) { + return $category; + })->all(); + + assertType('array', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-5529.php b/tests/PHPStan/Analyser/nsrt/bug-5529.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5529.php rename to tests/PHPStan/Analyser/nsrt/bug-5529.php diff --git a/tests/PHPStan/Analyser/data/bug-5530.php b/tests/PHPStan/Analyser/nsrt/bug-5530.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5530.php rename to tests/PHPStan/Analyser/nsrt/bug-5530.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5552.php b/tests/PHPStan/Analyser/nsrt/bug-5552.php new file mode 100644 index 0000000000..4b05d1b053 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5552.php @@ -0,0 +1,55 @@ +', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (A::class === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($mixed) === 'Bug5552\A') { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if ('Bug5552\A' === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($o) === A::class) { + assertType('Bug5552\A', $o); + } + if (A::class === get_parent_class($o)) { + assertType('Bug5552\A', $o); + } + + if (get_parent_class($s) === A::class) { + assertType('class-string', $s); + } + if (A::class === get_parent_class($s)) { + assertType('class-string', $s); + } + } +} + +class A {} diff --git a/tests/PHPStan/Analyser/data/bug-5584.php b/tests/PHPStan/Analyser/nsrt/bug-5584.php similarity index 80% rename from tests/PHPStan/Analyser/data/bug-5584.php rename to tests/PHPStan/Analyser/nsrt/bug-5584.php index 76632898e7..45e6efeaa3 100644 --- a/tests/PHPStan/Analyser/data/bug-5584.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5584.php @@ -19,6 +19,6 @@ public function unionSum(): void $b = ['b' => 6]; } - assertType("array()|array(?'b' => 6, ?'a' => 5)", $a + $b); + assertType('array{}|array{b?: 6, a?: 5}', $a + $b); } } diff --git a/tests/PHPStan/Analyser/data/bug-560.php b/tests/PHPStan/Analyser/nsrt/bug-560.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-560.php rename to tests/PHPStan/Analyser/nsrt/bug-560.php diff --git a/tests/PHPStan/Analyser/data/bug-5615.php b/tests/PHPStan/Analyser/nsrt/bug-5615.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5615.php rename to tests/PHPStan/Analyser/nsrt/bug-5615.php diff --git a/tests/PHPStan/Analyser/data/bug-5628.php b/tests/PHPStan/Analyser/nsrt/bug-5628.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5628.php rename to tests/PHPStan/Analyser/nsrt/bug-5628.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5668.php b/tests/PHPStan/Analyser/nsrt/bug-5668.php new file mode 100644 index 0000000000..65f66601bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5668.php @@ -0,0 +1,53 @@ + $in + */ + function has(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + + /** + * @param array $in + */ + function has2(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + + /** + * @param non-empty-array $in + */ + function has3(array $in, string $s): void + { + assertType('bool', in_array('test', $in, true)); + assertType('bool', in_array(rand() ? 'test' : 'bar', $in, true)); + assertType('bool', in_array($s, $in, true)); + } + + + /** + * @param non-empty-array $in + */ + function has4(array $in): void + { + assertType('true', in_array('test', $in, true)); + } + + /** + * @param non-empty-array $in + */ + function has5(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + +} diff --git a/tests/PHPStan/Analyser/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/data/bug-651.php b/tests/PHPStan/Analyser/nsrt/bug-651.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-651.php rename to tests/PHPStan/Analyser/nsrt/bug-651.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6556.php b/tests/PHPStan/Analyser/nsrt/bug-6556.php new file mode 100644 index 0000000000..d05d2fdad5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6556.php @@ -0,0 +1,42 @@ +' . $testArg[$option] . '

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

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

'; + } + } +} diff --git a/tests/PHPStan/Analyser/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 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug7580TypesPHP82; + +use function PHPStan\Testing\assertType; + +assertType('array{}', mb_str_split('', 1)); + +assertType('array{\'x\'}', mb_str_split('x', 1)); + +$v = (string) (mt_rand() === 0 ? '' : 'x'); +assertType('\'\'|\'x\'', $v); +assertType('array{}|array{\'x\'}', mb_str_split($v, 1)); + +function x(): string { throw new \Exception(); }; +$v = x(); +assertType('string', $v); +assertType('list', mb_str_split($v, 1)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7580.php b/tests/PHPStan/Analyser/nsrt/bug-7580.php new file mode 100644 index 0000000000..1bfc0b7544 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7580.php @@ -0,0 +1,20 @@ +', 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..5a654694cd --- /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($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..26fab127a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php @@ -0,0 +1,149 @@ + [ + self::GROUP_PUBLIC_CONSTANTS, + self::GROUP_PROTECTED_CONSTANTS, + self::GROUP_PRIVATE_CONSTANTS, + ], + self::GROUP_SHORTCUT_STATIC_PROPERTIES => [ + self::GROUP_PUBLIC_STATIC_PROPERTIES, + self::GROUP_PROTECTED_STATIC_PROPERTIES, + self::GROUP_PRIVATE_STATIC_PROPERTIES, + ], + self::GROUP_SHORTCUT_PROPERTIES => [ + self::GROUP_SHORTCUT_STATIC_PROPERTIES, + self::GROUP_PUBLIC_PROPERTIES, + self::GROUP_PROTECTED_PROPERTIES, + self::GROUP_PRIVATE_PROPERTIES, + ], + self::GROUP_SHORTCUT_PUBLIC_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PUBLIC_METHODS, + ], + self::GROUP_SHORTCUT_PROTECTED_METHODS => [ + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PROTECTED_METHODS, + ], + self::GROUP_SHORTCUT_PRIVATE_METHODS => [ + self::GROUP_PRIVATE_STATIC_METHODS, + self::GROUP_PRIVATE_METHODS, + ], + self::GROUP_SHORTCUT_FINAL_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + ], + self::GROUP_SHORTCUT_ABSTRACT_METHODS => [ + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + ], + self::GROUP_SHORTCUT_STATIC_METHODS => [ + self::GROUP_STATIC_CONSTRUCTORS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PRIVATE_STATIC_METHODS, + ], + self::GROUP_SHORTCUT_METHODS => [ + self::GROUP_SHORTCUT_FINAL_METHODS, + self::GROUP_SHORTCUT_ABSTRACT_METHODS, + self::GROUP_SHORTCUT_STATIC_METHODS, + self::GROUP_CONSTRUCTOR, + self::GROUP_DESTRUCTOR, + self::GROUP_PUBLIC_METHODS, + self::GROUP_PROTECTED_METHODS, + self::GROUP_PRIVATE_METHODS, + self::GROUP_MAGIC_METHODS, + ], + ]; + + /** + * @param array $supportedGroups + * @return array + */ + public function unpackShortcut(string $shortcut, array $supportedGroups): array + { + $groups = []; + + foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) { + if (in_array($groupOrShortcut, $supportedGroups, true)) { + $groups[] = $groupOrShortcut; + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } elseif ( + !array_key_exists($groupOrShortcut, self::SHORTCUTS) + ) { + // Nothing + assertType("array{constants: array{'public constants', 'protected constants', 'private constants'}, 'static properties': array{'public static properties', 'protected static properties', 'private static properties'}, properties: array{'static properties', 'public properties', 'protected properties', 'private properties'}, 'all public methods': array{'public final methods', 'public static final methods', 'public abstract methods', 'public static abstract methods', 'public static methods', 'public methods'}, 'all protected methods': array{'protected final methods', 'protected static final methods', 'protected abstract methods', 'protected static abstract methods', 'protected static methods', 'protected methods'}, 'all private methods': array{'private static methods', 'private methods'}, 'final methods': array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}, 'abstract methods': array{'public abstract methods', 'protected abstract methods', 'public static abstract methods', 'protected static abstract methods'}, 'static methods': array{'static constructors', 'public static final methods', 'protected static final methods', 'public static abstract methods', 'protected static abstract methods', 'public static methods', 'protected static methods', 'private static methods'}, methods: array{'final methods', 'abstract methods', 'static methods', 'constructor', 'destructor', 'public methods', 'protected methods', 'private methods', 'magic methods'}}", self::SHORTCUTS); + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } else { + $groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups)); + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } + } + + return $groups; + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-2.php b/tests/PHPStan/Analyser/nsrt/bug-7621-2.php new file mode 100644 index 0000000000..3a7926aa33 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-2.php @@ -0,0 +1,23 @@ + ['foo', 'bar'] ]; + + public function foo(): void + { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + $keys = [0, 1, 2]; + foreach ($keys as $key) { + if (array_key_exists($key, self::FOO['foo'])) { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } else { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-3.php b/tests/PHPStan/Analyser/nsrt/bug-7621-3.php new file mode 100644 index 0000000000..d6811657ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-3.php @@ -0,0 +1,20 @@ + ['foo', 'bar'] ]; + + + /** @param 'foo'|'bar' $key */ + public function foo(string $key): void + { + if (!array_key_exists($key, self::FOO)) { + assertType("array{foo: array{'foo', 'bar'}}", self::FOO); + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7639.php b/tests/PHPStan/Analyser/nsrt/bug-7639.php new file mode 100644 index 0000000000..68c575a62d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7639.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug7663; + +use function PHPStan\Testing\assertType; + +class HelloWorld8 +{ + /** + * @param 'de_DE'|'pretty-long' $str + */ + public function sayHello($str): void + { + assertType("''", substr('de_DE', 5, -5)); + assertType("'y'", substr('pretty-long', 5, -5)); + assertType("''|'y'", substr($str, 5, -5)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7663.php b/tests/PHPStan/Analyser/nsrt/bug-7663.php new file mode 100644 index 0000000000..3dda66aeca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7663.php @@ -0,0 +1,21 @@ += 8.0 + +namespace bug7685; + +use function PHPStan\Testing\assertType; + +interface Reader { + public function getFilePath(): string|false; +} + +function bug7685(Reader $reader): void { + $filePath = $reader->getFilePath(); + if (false !== (bool) $filePath) { + assertType('non-falsy-string', $filePath); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7688.php b/tests/PHPStan/Analyser/nsrt/bug-7688.php new file mode 100644 index 0000000000..cc0f7818a8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7688.php @@ -0,0 +1,50 @@ + + */ +function baz($value) +{ + if (is_int($value)) { + assertType('int', $value); + return $value < 1 ? 1 : $value; + } + + return $value; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7689.php b/tests/PHPStan/Analyser/nsrt/bug-7689.php new file mode 100644 index 0000000000..d7234af1d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7689.php @@ -0,0 +1,50 @@ +isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + + } else { + assertType('bool', $reader->read()); + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + assertType('bool', $reader->read()); + $reader->read(); + + return $values; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7698.php b/tests/PHPStan/Analyser/nsrt/bug-7698.php new file mode 100644 index 0000000000..b88ad68617 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7698.php @@ -0,0 +1,41 @@ +value::class; + assertType("'Bug7698\\\\A'|'Bug7698\\\\B'", $class); + + if ($class === A::class) { + return; + } + + assertType("'Bug7698\\\\B'", $class); + + if ($class === B::class) { + return; + } + + assertType('*NEVER*', $class); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7764.php b/tests/PHPStan/Analyser/nsrt/bug-7764.php new file mode 100644 index 0000000000..2583a46d6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7764.php @@ -0,0 +1,17 @@ + 1) { + echo 'Success', "\n"; + } + print_r($split); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7776.php b/tests/PHPStan/Analyser/nsrt/bug-7776.php new file mode 100644 index 0000000000..e01fa5f841 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7776.php @@ -0,0 +1,27 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7776; + +use function PHPStan\Testing\assertType; + +/** + * @param array{page?: int, search?: string} $settings + */ +function test(array $settings = []): bool { + $copy = [...$settings]; + assertType('array{page?: int, search?: string}', $copy); + assertType('array{page?: int, search?: string}', $settings); + return isset($copy['search']); +} + +/** + * @param array{page?: int, search?: string} $settings + */ +function test2(array $settings = []): bool { + $copy = ['page' => 1, ...$settings]; + assertType('array{page: int, search?: string}', $copy); + assertType('array{page?: int, search?: string}', $settings); + return isset($copy['search']); +} diff --git a/tests/PHPStan/Analyser/data/bug-778.php b/tests/PHPStan/Analyser/nsrt/bug-778.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-778.php rename to tests/PHPStan/Analyser/nsrt/bug-778.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-7788.php b/tests/PHPStan/Analyser/nsrt/bug-7788.php new file mode 100644 index 0000000000..fa5c6a73af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7788.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug7788; + +use function PHPStan\Testing\assertType; + +/** + * @template T of array + */ +final class Props +{ + /** + * @param T $props + */ + public function __construct(private array $props = []) + { + } + + /** + * @template K of key-of + * @template TDefault + * @param K $propKey + * @param TDefault $default + * @return T[K]|TDefault + */ + public function getProp(string $propKey, mixed $default = null): mixed + { + return $this->props[$propKey] ?? $default; + } +} + +function () { + assertType('int', (new Props(['title' => 'test', 'value' => 30]))->getProp('value', 0)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7805.php b/tests/PHPStan/Analyser/nsrt/bug-7805.php new file mode 100644 index 0000000000..ec9464ebd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7805.php @@ -0,0 +1,33 @@ +", $params); + $params = $params === [] ? ['list'] : $params; + assertType("array{'list'}", $params); + assertNativeType("array{'list'}", $params); + array_unshift($params, 'help'); + assertType("array{'help', 'list'}", $params); + assertNativeType("array{'help', 'list'}", $params); + } + assertType("array{}|array{'help', 'list'}", $params); + assertNativeType('array', $params); + + return $params; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7809.php b/tests/PHPStan/Analyser/nsrt/bug-7809.php new file mode 100644 index 0000000000..a6c0c139cb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7809.php @@ -0,0 +1,18 @@ + : non-falsy-string + * ) $v + * @return void + */ + function foo( $k, $v ) { + if ( $k === 'a' ) { + assertType('int<0, 1>', $v); + } else { + assertType('non-falsy-string', $v); + } + } +} + +class HelloWorld2 +{ + /** + * @param string|array $name + * @param ($name is array ? null : int) $value + */ + public function setConfig($name, $value): void + { + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } + + /** + * @param string|array $name + * @param int $value + */ + public function setConfigMimicConditionalParamType($name, $value): void + { + if (is_array($name)) { + $value = null; + } + + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } +} + +/** + * @param ($isArray is false ? string : array) $data + * + * @return ($isArray is false ? string : array) + */ +function to_utf8($data, bool $isArray = false) +{ + if ($isArray) { + assertType('array', $data); + if (is_array($data)) { // always true + foreach ($data as $k => $value) { + $data[$k] = to_utf8($value, is_array($value)); + } + } else { + assertType('*NEVER*', $data); + $data = []; // dead code + } + } else { + assertType('string', $data); + $data = @iconv('UTF-8', 'UTF-8//IGNORE', $data); + } + + return $data; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7921.php b/tests/PHPStan/Analyser/nsrt/bug-7921.php new file mode 100644 index 0000000000..0a95bc79f3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7921.php @@ -0,0 +1,41 @@ + $arr */ + public function sayHello(array $arr): void + { + $pre_computed_arr = [ + 'a' => null, + 'b' => null, + 'c' => null, + ]; + + foreach ($arr as $arr_val) { + $pre_computed_arr['a'] = $arr_val['a']; + $pre_computed_arr['b'] = $arr_val['b']; + $pre_computed_arr['c'] = $arr_val['c']; + } + + assertType('string|null', $pre_computed_arr['a']); + assertType('string|null', $pre_computed_arr['b']); + assertType('string|null', $pre_computed_arr['c']); + + if ($pre_computed_arr['a'] === null) { + assertType('null', $pre_computed_arr['a']); + assertType('null', $pre_computed_arr['b']); + assertType('null', $pre_computed_arr['c']); + return; + } + + assertType('string', $pre_computed_arr['a']); + assertType('string', $pre_computed_arr['b']); + assertType('string|null', $pre_computed_arr['c']); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7928.php b/tests/PHPStan/Analyser/nsrt/bug-7928.php new file mode 100644 index 0000000000..df5cee1214 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7928.php @@ -0,0 +1,29 @@ += 8.0 + +namespace Bug7944; + +use function PHPStan\Testing\assertType; + +/** + * @template TValue + */ +final class Value +{ + /** @var TValue */ + public readonly mixed $value; + + /** + * @param TValue $value + */ + public function __construct(mixed $value) + { + $this->value = $value; + } +} + +/** + * @param non-empty-string $p + */ +function test($p): void { + $value = new Value($p); + assertType('Bug7944\\Value', $value); +}; + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7949.php b/tests/PHPStan/Analyser/nsrt/bug-7949.php new file mode 100644 index 0000000000..2c1b056fea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7949.php @@ -0,0 +1,19 @@ + 0 ? $price : '0'; + assertType('non-empty-string', $price); + + $this->foo($price); + } + + public function foo(string $test): void { + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7963-three.php b/tests/PHPStan/Analyser/nsrt/bug-7963-three.php new file mode 100644 index 0000000000..75c7d9a05d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7963-three.php @@ -0,0 +1,29 @@ + $objectClass + * @return TypeObject + */ + public function getObject(string $objectClass): AbstractA|AbstractB + { + if (is_subclass_of($objectClass, AbstractA::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectA($objectClass); + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)', $object); + } elseif (is_subclass_of($objectClass, AbstractB::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectB($objectClass); + assertType('TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + } else { + throw new \Exception("unable to instantiate $objectClass"); + } + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)|TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + return $object; + } + + /** + * @template TypeObject of AbstractA + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectA(string $objectClass): AbstractA + { + return new $objectClass(); + } + + /** + * @template TypeObject of AbstractB + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectB(string $objectClass): AbstractB + { + return new $objectClass(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7993.php b/tests/PHPStan/Analyser/nsrt/bug-7993.php new file mode 100644 index 0000000000..03093f189b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7993.php @@ -0,0 +1,9 @@ + 0 === $value % 2)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7996.php b/tests/PHPStan/Analyser/nsrt/bug-7996.php new file mode 100644 index 0000000000..e336dee7f9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7996.php @@ -0,0 +1,31 @@ + $inputArray + * @return non-empty-array<\stdclass> + */ + public function filter(array $inputArray): array + { + $currentItem = reset($inputArray); + $outputArray = [$currentItem]; // $outputArray is now non-empty-array + assertType('array{stdclass}', $outputArray); + + while ($nextItem = next($inputArray)) { + if (rand(1, 2) === 1) { + assertType('non-empty-list', $outputArray); + // The fact that this is into an if, reverts type of $outputArray to array + $outputArray[] = $nextItem; + } + assertType('non-empty-list', $outputArray); + } + + assertType('non-empty-list', $outputArray); + return $outputArray; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8008.php b/tests/PHPStan/Analyser/nsrt/bug-8008.php new file mode 100644 index 0000000000..d3e8ae63d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8008.php @@ -0,0 +1,61 @@ + $items + */ + public function __construct( + public array $items, + ) { + } + + /** + * @return array + */ + public function all() { + return $this->items; + } +} + +/** + * @template TValue of object + * + * @mixin Collection + */ +class Paginator +{ + /** + * @var Collection + */ + public Collection $collection; + + /** + * @param array $items + */ + public function __construct(public array $items) + { + $this->collection = new Collection($items); + } +} + +class MyObject {} + + +function (): void { + $paginator = new Paginator([new MyObject()]); + + assertType('Bug8008\Paginator', $paginator); + assertType('array', $paginator->items); + assertType('Bug8008\Collection', $paginator->collection); + assertType('array', $paginator->collection->items); + + assertType('array', $paginator->all()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-801.php b/tests/PHPStan/Analyser/nsrt/bug-801.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-801.php rename to tests/PHPStan/Analyser/nsrt/bug-801.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-8015.php b/tests/PHPStan/Analyser/nsrt/bug-8015.php new file mode 100644 index 0000000000..99fa39d27b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8015.php @@ -0,0 +1,30 @@ + $items + * @return array + */ +function extractParameters(array $items): array +{ + $config = []; + foreach ($items as $itemName => $item) { + if (is_array($item)) { + $config['things'] = []; + assertType('array{}', $config['things']); + foreach ($item as $thing) { + assertType('list', $config['things']); + $config['things'][] = (string) $thing; + } + assertType('list', $config['things']); + } else { + $config[$itemName] = (string) $item; + } + } + assertType('list|string', $config['things']); + + return $config; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8017.php b/tests/PHPStan/Analyser/nsrt/bug-8017.php new file mode 100644 index 0000000000..dfa7284786 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8017.php @@ -0,0 +1,16 @@ + 0) { + assertType('array{dirname: string, basename: string, extension?: string, filename: string}', pathinfo($fileName)); + } + + return $pathinfo['dirname'] ?? ''; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8084.php b/tests/PHPStan/Analyser/nsrt/bug-8084.php new file mode 100644 index 0000000000..fe5869e8e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8084.php @@ -0,0 +1,20 @@ + $data + **/ + public function sayHello(array $data): bool + { + \PHPStan\dumpType($data); + assertType('array', $data); + + $data['uses'] = ['']; + + assertType("non-empty-array&hasOffsetValue('uses', array{''})", $data); + + $data['uses'][] = ''; + + assertType("non-empty-array&hasOffsetValue('uses', array{'', ''})", $data); + + return count($data['foo']) > 0; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8092.php b/tests/PHPStan/Analyser/nsrt/bug-8092.php new file mode 100644 index 0000000000..52bd9abe1e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8092.php @@ -0,0 +1,39 @@ + */ +class TypeWithSpecific implements TypeWithGeneric +{ + public function get(): Specific + { + return new Specific(); + } +} + +class HelloWorld +{ + /** @param TypeWithGeneric $type */ + public function test(TypeWithGeneric $type): void + { + match (get_class($type)) { + TypeWithSpecific::class => assertType(TypeWithSpecific::class, $type), + default => false, + }; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8127.php b/tests/PHPStan/Analyser/nsrt/bug-8127.php new file mode 100644 index 0000000000..6e38769e6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8127.php @@ -0,0 +1,52 @@ + + */ +final class SinkCollector implements Collector +{ + public function getNodeType(): string + { + return CallLike::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope) + {} +} + +class TaintType +{ + public const TYPE_INPUT = 'input'; + public const TYPE_SQL = 'sql'; + public const TYPE_HTML = 'html'; + + public const TYPES = [self::TYPE_INPUT, self::TYPE_SQL, self::TYPE_HTML]; +} + +/** + * @implements Rule + */ +final class TaintRule implements Rule +{ + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope): array + { + $sinkCollectorData = $node->get(SinkCollector::class); + assertType("array>", $sinkCollectorData); + + return []; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-82.php b/tests/PHPStan/Analyser/nsrt/bug-82.php new file mode 100644 index 0000000000..754d2244d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-82.php @@ -0,0 +1,23 @@ +, + * } $array + */ + public function sayHello(array $array, string $string): void + { + assertType('array{notImportant: bool, attributesRequiredLogistic?: array}', $array); + unset($array[$string]); + assertType('array{notImportant?: bool, attributesRequiredLogistic?: array}', $array); + } + + public function edgeCase(): void + { + $arr = [1,2,3]; + unset($arr['1']); + assertType('array{0: 1, 2: 3}', $arr); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8242.php b/tests/PHPStan/Analyser/nsrt/bug-8242.php new file mode 100644 index 0000000000..3e516a7199 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8242.php @@ -0,0 +1,60 @@ += 8.0 + +namespace Bug8249; + +use function PHPStan\Testing\assertType; + +function foo(): mixed +{ + return null; +} + +function () { + $x = foo(); + + if (is_int($x)) { + assertType('int', $x); + assertType('true', is_int($x)); + } else { + assertType('mixed~int', $x); + assertType('false', is_int($x)); + } +}; + +function () { + $x = ['x' => foo()]; + + if (is_int($x['x'])) { + assertType('array{x: int}', $x); + assertType('int', $x['x']); + assertType('true', is_int($x['x'])); + } else { + assertType('array{x: mixed~int}', $x); + assertType('mixed~int', $x['x']); + assertType('false', is_int($x['x'])); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8272.php b/tests/PHPStan/Analyser/nsrt/bug-8272.php new file mode 100644 index 0000000000..e26fba40f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8272.php @@ -0,0 +1,10 @@ +', mt_rand(1, 5)); + assertType('int<0, max>', mt_rand()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8361.php b/tests/PHPStan/Analyser/nsrt/bug-8361.php new file mode 100644 index 0000000000..cf0bb34b9b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8361.php @@ -0,0 +1,32 @@ +format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } + + public function sayHello2(?DateTimeInterface $from = null, ?DateTimeInterface $to = null): void + { + if ($from || $to) { + $operator = $from ? 'notBefore' : 'notAfter'; + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8366.php b/tests/PHPStan/Analyser/nsrt/bug-8366.php new file mode 100644 index 0000000000..fd6c65e972 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8366.php @@ -0,0 +1,36 @@ + $untilDate) { + assertType('DateTimeImmutable', $untilDate); + assertType('null', $count); + throw new \InvalidArgumentException('End date must not be greater than until date.'); + } + + if ($count !== null && $count < 1) { + assertType('null', $untilDate); + assertType('int', $count); + throw new \InvalidArgumentException('Count must be positive.'); + } + + assertType('DateTimeImmutable|null', $untilDate); + assertType('int<1, max>|null', $count); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8373.php b/tests/PHPStan/Analyser/nsrt/bug-8373.php new file mode 100644 index 0000000000..54471fb19f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8373.php @@ -0,0 +1,19 @@ +foo($a); + assertType('int', $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8442.php b/tests/PHPStan/Analyser/nsrt/bug-8442.php new file mode 100644 index 0000000000..96005d7d85 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8442.php @@ -0,0 +1,41 @@ + + * + * @phpstan-param list|null $mirrors + */ + protected function getUrls(?string $url, ?array $mirrors, ?string $ref, ?string $type, string $urlType): array + { + if (!$url) { + return []; + } + + if ($urlType === 'dist' && false !== strpos($url, '%')) { + assertType('string|null', $type); + $url = 'test'; + } + assertType('non-falsy-string', $url); + + $urls = [$url]; + if ($mirrors) { + foreach ($mirrors as $mirror) { + if ($urlType === 'dist') { + assertType('string|null', $type); + } elseif ($urlType === 'source' && $type === 'git') { + assertType("'git'", $type); + } elseif ($urlType === 'source' && $type === 'hg') { + assertType("'hg'", $type); + } else { + continue; + } + } + } + + return $urls; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8486.php b/tests/PHPStan/Analyser/nsrt/bug-8486.php new file mode 100644 index 0000000000..e15c8a6544 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8486.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug8486; + +use function PHPStan\Testing\assertType; + +enum Operator: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case None = ''; + + public function explode(): void + { + $character = match ($this) { + self::None => 'baz', + default => $this->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(): void + { + match ($this) { + self::None => 'baz', + default => assertType('($this(Bug8486\Operator)&Bug8486\Operator::Foo)|($this(Bug8486\Operator)&Bug8486\Operator::Bar)', $this), + }; + } + + public function typeInference2(): void + { + if ($this === self::None) { + return; + } + + assertType("'Bar'|'Foo'", $this->name); + assertType("'bar'|'foo'", $this->value); + } +} + +class Foo +{ + + public function doFoo(Operator $operator) + { + $character = match ($operator) { + Operator::None => 'baz', + default => $operator->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(Operator $operator): void + { + match ($operator) { + Operator::None => 'baz', + default => assertType('Bug8486\Operator::Bar|Bug8486\Operator::Foo', $operator), + }; + } + + public function typeInference2(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + assertType("'Bar'|'Foo'", $operator->name); + assertType("'bar'|'foo'", $operator->value); + } + + public function typeInference3(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + if ($operator === Operator::Foo) { + return; + } + + assertType("Bug8486\Operator::Bar", $operator); + assertType("'Bar'", $operator->name); + assertType("'bar'", $operator->value); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8517.php b/tests/PHPStan/Analyser/nsrt/bug-8517.php new file mode 100644 index 0000000000..ab6570b796 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8517.php @@ -0,0 +1,15 @@ +attributes->get('_route_params', []); + assertType('stdClass|null', $request); + $routeParams = $request?->attributes->get('_route_params', []) ?? []; + $param = $request?->attributes->get('_param') ?? $routeParams['_param']; + assertType('stdClass|null', $request); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8520.php b/tests/PHPStan/Analyser/nsrt/bug-8520.php new file mode 100644 index 0000000000..d5d6c605fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8520.php @@ -0,0 +1,15 @@ +', $i); + $tryMax = true; + while ($tryMax) { + $tryMax = false; + } +} + +assertType('int<7, max>', $i); diff --git a/tests/PHPStan/Analyser/nsrt/bug-8543.php b/tests/PHPStan/Analyser/nsrt/bug-8543.php new file mode 100644 index 0000000000..01d7b93c45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8543.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug8543; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public readonly int $i; + + public int $j; + + public function invalidate(): void + { + } +} + +function (HelloWorld $hw): void { + $hw->i = 1; + $hw->j = 2; + assertType('1', $hw->i); + assertType('2', $hw->j); + + $hw->invalidate(); + assertType('1', $hw->i); + assertType('int', $hw->j); + + $hw = new HelloWorld(); + assertType('int', $hw->i); + assertType('int', $hw->j); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8559.php b/tests/PHPStan/Analyser/nsrt/bug-8559.php new file mode 100644 index 0000000000..ee68b2fff0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8559.php @@ -0,0 +1,40 @@ + 1, 'b' => 2]; + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get(string $key): int + { + assert(isset(self::KEYS[$key])); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get2(string $key): int + { + assert(in_array($key, array_keys(self::KEYS), true)); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } +} + +$key = 'x'; +$v = X::get($key); +assertType("*NEVER*", $key); + +$key = 'a'; +$v = X::get($key); +assertType("'a'", $key); diff --git a/tests/PHPStan/Analyser/nsrt/bug-8568.php b/tests/PHPStan/Analyser/nsrt/bug-8568.php new file mode 100644 index 0000000000..9236447acf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8568.php @@ -0,0 +1,26 @@ +get()); + } + + public function get(): ?int + { + return rand() ? 5 : null; + } + + /** + * @param numeric-string $numericS + */ + public function intersections($numericS): void { + assertType('non-falsy-string', 'a'. $numericS); + assertType('numeric-string', (string) $numericS); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-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-8886.php b/tests/PHPStan/Analyser/nsrt/bug-8886.php new file mode 100644 index 0000000000..47a385dea5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8886.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug8886; + +use PDO; +use function PHPStan\Testing\assertType; + +function testPDOStatementGetIterator(): void { + $pdo = new PDO('sqlite::memory:'); + $stmt = $pdo->query('SELECT 1'); + + assertType('Iterator', $stmt->getIterator()); +} 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-8922-php74.php b/tests/PHPStan/Analyser/nsrt/bug-8922-php74.php new file mode 100644 index 0000000000..1a0af68920 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8922-php74.php @@ -0,0 +1,19 @@ + $array + * @param non-falsy-string $string + * @param mixed $mixed + */ +function doSomething($array, $string, $mixed): void +{ + assertType('list', mb_detect_order()); + assertType('bool', mb_detect_order(null)); + assertType('bool', mb_detect_order($array)); + assertType('bool', mb_detect_order($string)); + assertType('bool', mb_detect_order($mixed)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8922.php b/tests/PHPStan/Analyser/nsrt/bug-8922.php new file mode 100644 index 0000000000..d7e39b5a14 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8922.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug8922; + +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-list $array + * @param non-falsy-string $string + * @param mixed $mixed + */ +function doSomething($array, $string, $mixed): void +{ + assertType('list', mb_detect_order()); + assertType('list', mb_detect_order(null)); + assertType('bool', mb_detect_order($array)); + assertType('bool', mb_detect_order($string)); + assertType('bool|list', mb_detect_order($mixed)); +} 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-9204.php b/tests/PHPStan/Analyser/nsrt/bug-9204.php new file mode 100644 index 0000000000..165a274bf3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9204.php @@ -0,0 +1,37 @@ + + */ + private array $properties; + + /** + * @param array $properties + */ + public function __construct(array $properties = []) + { + $this->properties = $properties; + } + public function __isset(string $offset): bool + { + return isset($this->properties[$offset]); + } + public function __get(string $offset): mixed + { + return $this->properties[$offset] ?? null; + } + }); + $objects = [ + new $goodObject(['id' => 42]), + new $goodObject(['id' => 1337]), + ]; + + $columns = array_column($objects, 'id'); + assertType('list', $columns); +}; 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 + +declare(strict_types=1); + +namespace Bug9435; + +use function PHPStan\Testing\assertType; + +function x(): mixed +{ + return null; +} + +/** @phpstan-assert ($allow_null is true ? string|null : string) $input */ +function trueCheck(mixed $input, bool $allow_null = false): void +{ +} + +$a = x(); +trueCheck($a); +assertType('string', $a); // incorrect: should be string but is string|null + +$a = x(); +trueCheck($a, false); +assertType('string', $a); // correct (string) + +$a = x(); +trueCheck($a, allow_null: false); +assertType('string', $a); // correct (string) + +$a = x(); +trueCheck(allow_null: false, input: $a); +assertType('string', $a); // correct (string) + +$a = x(); +trueCheck($a, true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +trueCheck($a, allow_null: true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +trueCheck(allow_null: true, input: $a); +assertType('string|null', $a); // correct (string|null) + +/** @phpstan-assert ($allow_null is false ? string : string|null) $input */ +function falseCheck(mixed $input, bool $allow_null = false): void +{ +} + +$a = x(); +falseCheck($a); +assertType('string', $a); // incorrect: should be string but is string|null + +$a = x(); +falseCheck($a, false); +assertType('string', $a); // correct (string) + +$a = x(); +falseCheck($a, allow_null: false); +assertType('string', $a); // correct (string) + +$a = x(); +falseCheck(allow_null: false, input: $a); +assertType('string', $a); // correct (string|null) + +$a = x(); +falseCheck($a, true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +falseCheck($a, allow_null: true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +falseCheck(allow_null: true, input: $a); +assertType('string|null', $a); // correct (string|null) + +/** @phpstan-assert ($allow_null is not true ? string : string|null) $input */ +function notTrueCheck(mixed $input, bool $allow_null = false): void +{ +} + +$a = x(); +notTrueCheck($a); +assertType('string', $a); // incorrect: should be string but is string|null + +$a = x(); +notTrueCheck($a, false); +assertType('string', $a); // correct (string) + +$a = x(); +notTrueCheck($a, allow_null: false); +assertType('string', $a); // correct (string) + +$a = x(); +notTrueCheck(allow_null: false, input: $a); +assertType('string', $a); // correct (string|null) + +$a = x(); +notTrueCheck($a, true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +notTrueCheck($a, allow_null: true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +notTrueCheck(allow_null: true, input: $a); +assertType('string|null', $a); // correct (string|null) + +/** @phpstan-assert ($allow_null is not false ? string|null : string) $input */ +function notFalseCheck(mixed $input, bool $allow_null = false): void +{ +} + +$a = x(); +notFalseCheck($a); +assertType('string', $a); // incorrect: should be string but is string|null + +$a = x(); +notFalseCheck($a, false); +assertType('string', $a); // correct (string) + +$a = x(); +notFalseCheck($a, allow_null: false); +assertType('string', $a); // correct (string) + +$a = x(); +notFalseCheck(allow_null: false, input: $a); +assertType('string', $a); // correct (string|null) + +$a = x(); +notFalseCheck($a, true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +notFalseCheck($a, allow_null: true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +notFalseCheck(allow_null: true, input: $a); +assertType('string|null', $a); // correct (string|null) + +/** @phpstan-assert ($allow_null is false ? string : string|null) $input */ +function checkWithVariadics(mixed $input, bool $allow_null = false, ...$more): void +{ +} + +$a = x(); +checkWithVariadics($a); +assertType('string', $a); // incorrect: should be string but is string|null + +$a = x(); +checkWithVariadics($a, false); +assertType('string', $a); // correct (string) + +$a = x(); +checkWithVariadics($a, allow_null: false); +assertType('string', $a); // correct (string) + +$a = x(); +checkWithVariadics(allow_null: false, input: $a); +assertType('string', $a); // correct (string) + +$a = x(); +checkWithVariadics($a, true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +checkWithVariadics($a, allow_null: true); +assertType('string|null', $a); // correct (string|null) + +$a = x(); +checkWithVariadics(allow_null: true, input: $a); +assertType('string|null', $a); // correct (string|null) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9456.php b/tests/PHPStan/Analyser/nsrt/bug-9456.php new file mode 100644 index 0000000000..22b5e4dad8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9456.php @@ -0,0 +1,71 @@ += 8.0 + +namespace Bug9456; + +use ArrayAccess; + +use function PHPStan\Testing\assertType; + +/** + * @template TKey of array-key + * @template TValue of mixed + * + * @implements ArrayAccess + */ +class Collection implements ArrayAccess { + /** + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param TValue|string|null $operator + * @param TValue|null $value + * @return static, static> + */ + public function partition($key, $operator = null, $value = null) {} // @phpstan-ignore-line + + /** + * @param TKey $key + * @return TValue + */ + public function offsetGet($key): mixed { return null; } // @phpstan-ignore-line + + /** + * @param TKey $key + * @return bool + */ + public function offsetExists($key): bool { return true; } + + /** + * @param TKey|null $key + * @param TValue $value + * @return void + */ + public function offsetSet($key, $value): void {} + + /** + * @param TKey $key + * @return void + */ + public function offsetUnset($key): void {} +} + +class HelloWorld +{ + /** + * @param Collection $collection + */ + public function sayHello(Collection $collection): void + { + $result = $collection->partition('key'); + + assertType( + 'Bug9456\Collection, Bug9456\Collection>', + $result + ); + assertType('Bug9456\Collection', $result[0]); + assertType('Bug9456\Collection', $result[1]); + + [$one, $two] = $collection->partition('key'); + + assertType('Bug9456\Collection', $one); + assertType('Bug9456\Collection', $two); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9472.php b/tests/PHPStan/Analyser/nsrt/bug-9472.php new file mode 100644 index 0000000000..e81f67b7ea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9472.php @@ -0,0 +1,74 @@ += 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-9575.php b/tests/PHPStan/Analyser/nsrt/bug-9575.php new file mode 100644 index 0000000000..fb19202e4f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9575.php @@ -0,0 +1,22 @@ + + 1 +
+XML; + +$xml = new SimpleXMLElement($string); +foreach($xml->foo[0]->attributes() as $a => $b) { + echo $a,'="',$b,"\"\n"; +} + +assertType('(SimpleXMLElement|null)', $xml->foo); +assertType('(SimpleXMLElement|null)', $xml->foo[0]); +assertType('(SimpleXMLElement|null)', $xml->foobar); +assertType('(SimpleXMLElement|null)', $xml->foo->attributes()); 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..5629966029 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9714.php @@ -0,0 +1,15 @@ +xpath('//data'); + assertType('array|null', $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..52c3dd1c20 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9734.php @@ -0,0 +1,147 @@ + $a + * @return void + */ + public function doFoo(array $a): void + { + if (array_is_list($a)) { + assertType('list', $a); + } else { + assertType('non-empty-array', $a); + } + } + + 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); + } + } + + /** + * @param mixed $a + * @return void + */ + public function doFoo4($a): void + { + if (array_is_list($a)) { + assertType('list', $a); + } else { + assertType('mixed~list', $a); + } + } + +} + +class Bar +{ + /** + * @param array $value + * @return ($value is non-empty-list ? true : false) + */ + public function assertIsNonEmptyList($value): bool + { + return false; + } + + /** + * @param array $value + * @return ($value is list ? true : false) + */ + public function assertIsStringList($value): bool + { + return false; + } + + /** + * @param array $value + * @return ($value is list{string, string} ? true : false) + */ + public function assertIsConstantList($value): bool + { + return false; + } + + /** + * @param array $value + * @return ($value is list{0?: string, 1?: string} ? true : false) + */ + public function assertIsOptionalConstantList($value): bool + { + return false; + } + + /** + * @param array $value + * @return ($value is array ? true : false) + */ + public function assertIsStringArray($value): bool + { + return false; + } + + /** + * @param array $a + * @return void + */ + public function doFoo(array $a): void + { + if ($this->assertIsNonEmptyList($a)) { + assertType('non-empty-list', $a); + } else { + assertType('array', $a); + } + + if ($this->assertIsStringList($a)) { + assertType('list', $a); + } else { + assertType('non-empty-array', $a); + } + + if ($this->assertIsConstantList($a)) { + assertType('array{string, string}', $a); + } else { + assertType('array', $a); + } + + if ($this->assertIsOptionalConstantList($a)) { + assertType('list{0?: string, 1?: string}', $a); + } else { + assertType('non-empty-array', $a); + } + + if ($this->assertIsStringArray($a)) { + assertType('array', $a); + } else { + assertType('non-empty-array', $a); + } + } + + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9753.php b/tests/PHPStan/Analyser/nsrt/bug-9753.php new file mode 100644 index 0000000000..0d521cd960 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9753.php @@ -0,0 +1,24 @@ +|null', $items); + if (isset($items)) { + if (count($items) > 2) { + $items = null; + } else { + $items[] = $entry; + } + } + assertType('non-empty-list<1|2|3|4|5>|null', $items); + } + + assertType('non-empty-list<1|2|3|4|5>|null', $items); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9764.php b/tests/PHPStan/Analyser/nsrt/bug-9764.php new file mode 100644 index 0000000000..f24b810fe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9764.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug9764; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param callable(): T $fnc + * @return T + */ +function result(callable $fnc): mixed +{ + return $fnc(); +} + +function (): void { + /** @var array $a */ + $a = []; + $c = static fn (): array => $a; + assertType('Closure(): array', $c); + + $r = result($c); + assertType('array', $r); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9778.php b/tests/PHPStan/Analyser/nsrt/bug-9778.php new file mode 100644 index 0000000000..240fb5bbc7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9778.php @@ -0,0 +1,29 @@ + $articles, + 'farben' => null, + 'artikel_ids' => [], + ]; + + // collect article ids + foreach ($result['artikel'] as $article) { + $result['artikel_ids'][] = 1; + } + + assertType('array{artikel: Iterator, farben: null, artikel_ids: list<1>}', $result); + assertType('list<1>', $result['artikel_ids']); + + if ($result['artikel_ids'] !== []) { + $result['farben'] = new stdClass(); + } + + // $result['farben'] might be also null + assertType('stdClass|null', $result['farben']); + if ($result['farben'] instanceof stdClass) { + echo '123'; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9867.php b/tests/PHPStan/Analyser/nsrt/bug-9867.php new file mode 100644 index 0000000000..6ab9515b87 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9867.php @@ -0,0 +1,77 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug9867; + +use function PHPStan\Testing\assertType; + +/** @extends \SplMinHeap<\DateTime> */ +class MyMinHeap extends \SplMinHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplMaxHeap<\DateTime> */ +class MyMaxHeap extends \SplMaxHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplHeap<\DateTime> */ +abstract class MyHeap extends \SplHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-987.php b/tests/PHPStan/Analyser/nsrt/bug-987.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-987.php rename to tests/PHPStan/Analyser/nsrt/bug-987.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9870.php b/tests/PHPStan/Analyser/nsrt/bug-9870.php new file mode 100644 index 0000000000..420c346ef6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9870.php @@ -0,0 +1,43 @@ + $date + */ + public function sayHello($date): void + { + if (is_string($date)) { + assertType('non-empty-string', str_replace('-', '/', $date)); + } else { + assertType('list', str_replace('-', '/', $date)); + } + assertType('list|non-empty-string', str_replace('-', '/', $date)); + } + + /** + * @param string|array $stringOrArray + * @param non-empty-string|array $nonEmptyStringOrArray + * @param string|array $stringOrArrayNonEmptyString + * @param string|non-empty-array $stringOrNonEmptyArray + * @param string|array|bool|int $wrongParam + */ + public function moreCheck( + $stringOrArray, + $nonEmptyStringOrArray, + $stringOrArrayNonEmptyString, + $stringOrNonEmptyArray, + $wrongParam, + ): void { + assertType('array|string', str_replace('-', '/', $stringOrArray)); + assertType('array|non-empty-string', str_replace('-', '/', $nonEmptyStringOrArray)); + assertType('array|string', str_replace('-', '/', $stringOrArrayNonEmptyString)); + assertType('non-empty-array|string', str_replace('-', '/', $stringOrNonEmptyArray)); + assertType('array|string', str_replace('-', '/', $wrongParam)); + assertType('array|string', str_replace('-', '/')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9881.php b/tests/PHPStan/Analyser/nsrt/bug-9881.php new file mode 100644 index 0000000000..129ca9c87f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9881.php @@ -0,0 +1,27 @@ += 8.1 + +namespace Bug9881; + +use BackedEnum; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * @template B of BackedEnum + * @param B[] $enums + * @return value-of[] + */ + public static function arrayEnumToStrings(array $enums): array + { + return array_map(static fn (BackedEnum $code): string|int => $code->value, $enums); + } +} + +enum Test: string { + case DA = 'da'; +} + +function (Test ...$da): void { + assertType('array<\'da\'>', HelloWorld::arrayEnumToStrings($da)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9939.php b/tests/PHPStan/Analyser/nsrt/bug-9939.php new file mode 100644 index 0000000000..5f828bebd2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9939.php @@ -0,0 +1,65 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug9939; + +use function PHPStan\Testing\assertType; + +enum Combinator +{ + case NEXT_SIBLING; + case CHILD; + case FOLLOWING_SIBLING; + + public function getText(): string + { + return match ($this) { + self::NEXT_SIBLING => '+', + self::CHILD => '>', + self::FOLLOWING_SIBLING => '~', + }; + } +} + +/** + * @template T of string|\Stringable|array|Combinator|null + */ +class CssValue +{ + /** + * @param T $value + */ + public function __construct(private readonly mixed $value) + { + } + + /** + * @return T + */ + public function getValue(): mixed + { + return $this->value; + } + + public function __toString(): string + { + assertType('T of array|Bug9939\Combinator|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if ($this->value instanceof Combinator) { + assertType('T of Bug9939\Combinator (class Bug9939\CssValue, argument)', $this->value); + return $this->value->getText(); + } + + assertType('T of array|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if (\is_array($this->value)) { + assertType('T of array (class Bug9939\CssValue, argument)', $this->value); + return implode($this->value); + } + + assertType('T of string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + return (string) $this->value; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9963.php b/tests/PHPStan/Analyser/nsrt/bug-9963.php new file mode 100644 index 0000000000..e5d8444afd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9963.php @@ -0,0 +1,23 @@ +|Bug9963\HelloWorld|false', $h->find($something)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9985.php b/tests/PHPStan/Analyser/nsrt/bug-9985.php new file mode 100644 index 0000000000..09a7ad92ea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9985.php @@ -0,0 +1,25 @@ += 1) { + $warnings['a'] = true; + } + + if (rand(0, 100) >= 2) { + $warnings['b'] = true; + } elseif (rand(0, 100) >= 3) { + $warnings['c'] = true; + } + + assertType('array{}|array{a?: true, b: true}|array{a?: true, c?: true}', $warnings); + + if (!empty($warnings)) { + assertType('array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', $warnings); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9995.php b/tests/PHPStan/Analyser/nsrt/bug-9995.php new file mode 100644 index 0000000000..c4fe4d6ada --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9995.php @@ -0,0 +1,15 @@ +format('c'); +} diff --git a/tests/PHPStan/Analyser/data/bug-empty-array.php b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-empty-array.php rename to tests/PHPStan/Analyser/nsrt/bug-empty-array.php index c50df67513..91a6b8bb00 100644 --- a/tests/PHPStan/Analyser/data/bug-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php @@ -14,9 +14,9 @@ public function doFoo(): void { assertType('array', $this->comments); $this->comments = []; - assertType('array()', $this->comments); + assertType('array{}', $this->comments); if ($this->comments === []) { - assertType('array()', $this->comments); + assertType('array{}', $this->comments); return; } else { assertType('*NEVER*', $this->comments); @@ -29,9 +29,9 @@ public function doBar(): void { assertType('array', $this->comments); $this->comments = []; - assertType('array()', $this->comments); + assertType('array{}', $this->comments); if ([] === $this->comments) { - assertType('array()', $this->comments); + assertType('array{}', $this->comments); return; } else { assertType('*NEVER*', $this->comments); diff --git a/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php new file mode 100644 index 0000000000..82639be3e9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php @@ -0,0 +1,29 @@ += 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); +} diff --git a/tests/PHPStan/Analyser/data/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php similarity index 75% rename from tests/PHPStan/Analyser/data/bug-pr-339.php rename to tests/PHPStan/Analyser/nsrt/bug-pr-339.php index 01e3822e96..4d841ad783 100644 --- a/tests/PHPStan/Analyser/data/bug-pr-339.php +++ b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php @@ -17,17 +17,17 @@ assertType('mixed', $a); assertType('mixed', $c); if ($a) { - assertType("mixed~0|0.0|''|'0'|array()|false|null", $a); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $a); assertType('mixed', $c); assertVariableCertainty(TrinaryLogic::createYes(), $a); } if ($c) { assertType('mixed', $a); - assertType("mixed~0|0.0|''|'0'|array()|false|null", $c); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $c); assertVariableCertainty(TrinaryLogic::createYes(), $c); } } else { - assertType("0|0.0|''|'0'|array()|false|null", $a); - assertType("0|0.0|''|'0'|array()|false|null", $c); + assertType("0|0.0|''|'0'|array{}|false|null", $a); + assertType("0|0.0|''|'0'|array{}|false|null", $c); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-to-string-type.php b/tests/PHPStan/Analyser/nsrt/bug-to-string-type.php new file mode 100644 index 0000000000..31e1b97281 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-to-string-type.php @@ -0,0 +1,36 @@ +__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 100% rename from tests/PHPStan/Analyser/data/bug2574.php rename to tests/PHPStan/Analyser/nsrt/bug2574.php diff --git a/tests/PHPStan/Analyser/data/bug2577.php b/tests/PHPStan/Analyser/nsrt/bug2577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug2577.php rename to tests/PHPStan/Analyser/nsrt/bug2577.php diff --git a/tests/PHPStan/Analyser/nsrt/bug7856.php b/tests/PHPStan/Analyser/nsrt/bug7856.php new file mode 100644 index 0000000000..8da8b7343e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug7856.php @@ -0,0 +1,16 @@ +", $intervals); + $periodEnd = $periodEnd->modify(array_shift($intervals)); + } while (count($intervals) > 0 && $periodEnd->format('U') < $endDate); +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php new file mode 100644 index 0000000000..756185972c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php @@ -0,0 +1,26 @@ +', call_user_func('CallUserFuncPhp7\generic', $params)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php b/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php new file mode 100644 index 0000000000..3550b21d56 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php @@ -0,0 +1,55 @@ += 8.0 + +namespace CallUserFuncPhp8; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param T $t + * @return T + */ +function generic($t) { + return $t; +} + +/** + * @template T + * @param T $t + * @return T + */ +function generic3($t = '', int $b = 100, string $c = '') { + return $t; +} + + +function fun3($a = '', $b = '', $c = ''): int { + return 1; +} + +class Foo { + + /** + * @param string $params,... + */ + function doVariadics(...$params) { + // because of named arguments support in php8 we have a different return type as in php7 + // see https://phpstan.org/r/58c30346-9568-47ca-82e5-53b2fffda7d0 + assertType('array', call_user_func('CallUserFuncPhp8\generic', $params)); + } + + function doNamed() { + assertType('1', call_user_func('CallUserFuncPhp8\generic', t: 1)); + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic', t: [1, 2, 3])); + + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic3', t: [1, 2, 3])); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', b: 150)); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', c: 'lala')); + assertType('\'\'', call_user_func(c: 'lala', callback: 'CallUserFuncPhp8\generic3')); + + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', b: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', c: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3], c: 'c')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func.php b/tests/PHPStan/Analyser/nsrt/call-user-func.php new file mode 100644 index 0000000000..b54952ce4c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func.php @@ -0,0 +1,63 @@ +', call_user_func('CallUserFunc\generic', $strings)); + + assertType('int', call_user_func('CallUserFunc\fun')); + assertType('int', call_user_func('CallUserFunc\fun3', 1 ,2 ,3)); + assertType('string', call_user_func(['CallUserFunc\c', 'm'])); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/callable-in-union.php b/tests/PHPStan/Analyser/nsrt/callable-in-union.php new file mode 100644 index 0000000000..724ce9cafa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/callable-in-union.php @@ -0,0 +1,32 @@ +|(callable(array): array) $_ */ +function acceptArrayOrCallable($_) +{ +} + +acceptArrayOrCallable(fn ($parameter) => assertType('array', $parameter)); + +acceptArrayOrCallable(function ($parameter) { + assertType('array', $parameter); + return $parameter; +}); + +/** + * @param (callable(string): void)|callable(int): void $a + * @return void + */ +function acceptCallableOrCallableLikeArray($a): void +{ + +} + +acceptCallableOrCallableLikeArray(function ($p) { + assertType('int|string', $p); +}); + +acceptCallableOrCallableLikeArray(fn ($p) => assertType('int|string', $p)); diff --git a/tests/PHPStan/Analyser/nsrt/callable-object.php b/tests/PHPStan/Analyser/nsrt/callable-object.php new file mode 100644 index 0000000000..f9f3dee69c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/callable-object.php @@ -0,0 +1,38 @@ +|numeric-string|true', $mixed); + } else { + assertType('mixed~(int<0, max>|numeric-string|true)', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((int) $mixed)) { + assertType('mixed', $mixed); // could be *NEVER* + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((string) $int)) { + assertType('int', $int); + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((int) $int)) { + assertType('int', $int); // could be *NEVER* + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((string) $string)) { + assertType('numeric-string', $string); + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((int) $string)) { + assertType('string', $string); // could be *NEVER* + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((string) $numericString)) { + assertType('numeric-string', $numericString); + } else { + assertType('*NEVER*', $numericString); + } + assertType('numeric-string', $numericString); + + if (ctype_digit((string) $bool)) { + assertType('true', $bool); + } else { + assertType('false', $bool); + } + assertType('bool', $bool); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php new file mode 100644 index 0000000000..a1eb8830a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php @@ -0,0 +1,35 @@ + $positive + * @param int $negative + */ +function integerRangeToString($positive, $negative) +{ + assertType('lowercase-string&numeric-string&uppercase-string', (string) $positive); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $negative); + + if ($positive !== 0) { + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $positive); + } + if ($negative !== 0) { + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $negative); + } +} diff --git a/tests/PHPStan/Analyser/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/data/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php similarity index 91% rename from tests/PHPStan/Analyser/data/class-constant-types.php rename to tests/PHPStan/Analyser/nsrt/class-constant-types.php index 9d60af25c5..8f19f7659e 100644 --- a/tests/PHPStan/Analyser/data/class-constant-types.php +++ b/tests/PHPStan/Analyser/nsrt/class-constant-types.php @@ -15,6 +15,9 @@ class Foo /** @var string */ private const PRIVATE_TYPE = 'foo'; + /** @final */ + const FINAL_TYPE = 'zoo'; + public function doFoo() { assertType('1', self::NO_TYPE); @@ -28,6 +31,10 @@ public function doFoo() assertType('\'foo\'', self::PRIVATE_TYPE); assertType('string', static::PRIVATE_TYPE); assertType('string', $this::PRIVATE_TYPE); + + assertType('\'zoo\'', self::FINAL_TYPE); + assertType('\'zoo\'', static::FINAL_TYPE); + assertType('\'zoo\'', $this::FINAL_TYPE); } } diff --git a/tests/PHPStan/Analyser/nsrt/class-implements.php b/tests/PHPStan/Analyser/nsrt/class-implements.php new file mode 100644 index 0000000000..316c8e8ed4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-implements.php @@ -0,0 +1,128 @@ += 8.0 + +namespace ClassImplements; + +use function PHPStan\Testing\assertType; + +class ClassImplements +{ + /** + * @param object|class-string $objectOrClassString + * @param class-string $classString + */ + public function test( + object $object, + object|string $objectOrClassString, + object|string $objectOrString, + string $classString, + string $string, + bool $bool, + mixed $mixed, + ): void { + assertType('array', class_implements($object)); + assertType('(array|false)', class_implements($objectOrClassString)); + assertType('array|false', class_implements($objectOrString)); + assertType('(array|false)', class_implements($classString)); + assertType('array|false', class_implements($string)); + assertType('false', class_implements('thisIsNotAClass')); + + assertType('array', class_implements($object, true)); + assertType('(array|false)', class_implements($objectOrClassString, true)); + assertType('array|false', class_implements($objectOrString, true)); + assertType('(array|false)', class_implements($classString, true)); + assertType('array|false', class_implements($string, true)); + assertType('false', class_implements('thisIsNotAClass', true)); + + assertType('array', class_implements($object, false)); + assertType('array|false', class_implements($objectOrClassString, false)); + assertType('array|false', class_implements($objectOrString, false)); + assertType('array|false', class_implements($classString, false)); + assertType('array|false', class_implements($string, false)); + assertType('false', class_implements('thisIsNotAClass', false)); + + assertType('array', class_implements($object, $bool)); + assertType('array|false', class_implements($objectOrClassString, $bool)); + assertType('array|false', class_implements($objectOrString, $bool)); + assertType('array|false', class_implements($classString, $bool)); + assertType('array|false', class_implements($string, $bool)); + assertType('false', class_implements('thisIsNotAClass', $bool)); + + assertType('array', class_implements($object, $mixed)); + assertType('array|false', class_implements($objectOrClassString, $mixed)); + assertType('array|false', class_implements($objectOrString, $mixed)); + assertType('array|false', class_implements($classString, $mixed)); + assertType('array|false', class_implements($string, $mixed)); + assertType('false', class_implements('thisIsNotAClass', $mixed)); + + assertType('array', class_uses($object)); + assertType('(array|false)', class_uses($objectOrClassString)); + assertType('array|false', class_uses($objectOrString)); + assertType('(array|false)', class_uses($classString)); + assertType('array|false', class_uses($string)); + assertType('false', class_uses('thisIsNotAClass')); + + assertType('array', class_uses($object, true)); + assertType('(array|false)', class_uses($objectOrClassString, true)); + assertType('array|false', class_uses($objectOrString, true)); + assertType('(array|false)', class_uses($classString, true)); + assertType('array|false', class_uses($string, true)); + assertType('false', class_uses('thisIsNotAClass', true)); + + assertType('array', class_uses($object, false)); + assertType('array|false', class_uses($objectOrClassString, false)); + assertType('array|false', class_uses($objectOrString, false)); + assertType('array|false', class_uses($classString, false)); + assertType('array|false', class_uses($string, false)); + assertType('false', class_uses('thisIsNotAClass', false)); + + assertType('array', class_uses($object, $bool)); + assertType('array|false', class_uses($objectOrClassString, $bool)); + assertType('array|false', class_uses($objectOrString, $bool)); + assertType('array|false', class_uses($classString, $bool)); + assertType('array|false', class_uses($string, $bool)); + assertType('false', class_uses('thisIsNotAClass', $bool)); + + assertType('array', class_uses($object, $mixed)); + assertType('array|false', class_uses($objectOrClassString, $mixed)); + assertType('array|false', class_uses($objectOrString, $mixed)); + assertType('array|false', class_uses($classString, $mixed)); + assertType('array|false', class_uses($string, $mixed)); + assertType('false', class_uses('thisIsNotAClass', $mixed)); + + assertType('array', class_parents($object)); + assertType('(array|false)', class_parents($objectOrClassString)); + assertType('array|false', class_parents($objectOrString)); + assertType('(array|false)', class_parents($classString)); + assertType('array|false', class_parents($string)); + assertType('false', class_parents('thisIsNotAClass')); + + assertType('array', class_parents($object, true)); + assertType('(array|false)', class_parents($objectOrClassString, true)); + assertType('array|false', class_parents($objectOrString, true)); + assertType('(array|false)', class_parents($classString, true)); + assertType('array|false', class_parents($string, true)); + assertType('false', class_parents('thisIsNotAClass', true)); + + assertType('array', class_parents($object, false)); + assertType('array|false', class_parents($objectOrClassString, false)); + assertType('array|false', class_parents($objectOrString, false)); + assertType('array|false', class_parents($classString, false)); + assertType('array|false', class_parents($string, false)); + assertType('false', class_parents('thisIsNotAClass', false)); + + assertType('array', class_parents($object, $bool)); + assertType('array|false', class_parents($objectOrClassString, $bool)); + assertType('array|false', class_parents($objectOrString, $bool)); + assertType('array|false', class_parents($classString, $bool)); + assertType('array|false', class_parents($string, $bool)); + assertType('false', class_parents('thisIsNotAClass', $bool)); + + assertType('array', class_parents($object, $mixed)); + assertType('array|false', class_parents($objectOrClassString, $mixed)); + assertType('array|false', class_parents($objectOrString, $mixed)); + assertType('array|false', class_parents($classString, $mixed)); + assertType('array|false', class_parents($string, $mixed)); + assertType('false', class_parents('thisIsNotAClass', $mixed)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php b/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php new file mode 100644 index 0000000000..2523461229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php @@ -0,0 +1,14 @@ +createIdentifier('test')); + + if ($location->value === ClassNameUsageLocation::INSTANTIATION || $location->value === ClassNameUsageLocation::PROPERTY_TYPE) { + assertType("'new.test'|'property.test'", $location->createIdentifier('test')); + } +}; diff --git a/tests/PHPStan/Analyser/data/class-reflection-interfaces.php b/tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-reflection-interfaces.php rename to tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php diff --git a/tests/PHPStan/Analyser/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/data/classPhpDocs.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php similarity index 95% rename from tests/PHPStan/Analyser/data/classPhpDocs.php rename to tests/PHPStan/Analyser/nsrt/classPhpDocs.php index 0d447a3d49..f0024022ce 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs.php +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php @@ -9,6 +9,7 @@ * @method array arrayOfStrings() * @psalm-method array arrayOfStrings() * @phpstan-method array arrayOfInts() + * @phan-method array arrayOfStrings() * @method array arrayOfInts() * @method mixed overrodeMethod() * @method static mixed overrodeStaticMethod() diff --git a/tests/PHPStan/Analyser/data/clear-stat-cache.php b/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php similarity index 100% rename from tests/PHPStan/Analyser/data/clear-stat-cache.php rename to tests/PHPStan/Analyser/nsrt/clear-stat-cache.php diff --git a/tests/PHPStan/Analyser/nsrt/cli-globals.php b/tests/PHPStan/Analyser/nsrt/cli-globals.php new file mode 100644 index 0000000000..9dc930c395 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/cli-globals.php @@ -0,0 +1,33 @@ +', $argc); +assertType('non-empty-list', $argv); + +function f() { + assertType('*ERROR*', $argc); + assertType('*ERROR*', $argv); +} + +function g($argc, $argv) { + assertType('mixed', $argc); + assertType('mixed', $argv); +} + +function h() { + global $argc, $argv; + assertType('mixed', $argc); // should be int<1, max> + assertType('mixed', $argv); // should be non-empty-array +} + +function i() { + // user created local variable + $argc = 'hallo'; + $argv = 'welt'; + + assertType("'hallo'", $argc); + assertType("'welt'", $argv); +} diff --git a/tests/PHPStan/Analyser/nsrt/closure-argument-type.php b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php new file mode 100644 index 0000000000..b24570b298 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php @@ -0,0 +1,44 @@ + $items + * @param callable(T): U $cb + * @return array + */ + public function doFoo(array $items, callable $cb) + { + + } + + public function doBar() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, function ($item) { + assertType('1|2|3', $item); + return $item; + }); + assertType('array<1|2|3>', $b); + } + + public function doBaz() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, fn ($item) => $item); + assertType('array<1|2|3>', $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php b/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php new file mode 100644 index 0000000000..0db3aa730e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php @@ -0,0 +1,23 @@ +call($newThis, new class {}); assertType('true', $returnType); + +$staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); +assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive); diff --git a/tests/PHPStan/Analyser/data/closure-return-type.php b/tests/PHPStan/Analyser/nsrt/closure-return-type.php similarity index 91% rename from tests/PHPStan/Analyser/data/closure-return-type.php rename to tests/PHPStan/Analyser/nsrt/closure-return-type.php index c06880defa..386fec990c 100644 --- a/tests/PHPStan/Analyser/data/closure-return-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-return-type.php @@ -27,7 +27,7 @@ public function doFoo(int $i): void $f = function (): array { return ['foo' => 'bar']; }; - assertType('array(\'foo\' => \'bar\')', $f()); + assertType('array{foo: \'bar\'}', $f()); $f = function (string $s) { return $s; @@ -111,12 +111,12 @@ public function doBaz(): void $f = function() { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function(): void { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function() { if (rand(0, 1)) { @@ -134,7 +134,7 @@ public function doBaz(): void $this->returnNever(); }; - assertType('*NEVER*', $f([])); + assertType('never', $f([])); $f = function(array $a) { foreach ($a as $v) { @@ -148,12 +148,12 @@ public function doBaz(): void $this->returnNever(); } }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function (): \stdClass { throw new \Exception(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); } } diff --git a/tests/PHPStan/Analyser/nsrt/closure-types.php b/tests/PHPStan/Analyser/nsrt/closure-types.php new file mode 100644 index 0000000000..64a168e99a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-types.php @@ -0,0 +1,70 @@ + */ + private $arrayShapes; + + public function doFoo(): void + { + $a = array_map(function (array $a): array { + assertType('array{foo: string, bar: int}', $a); + + return $a; + }, $this->arrayShapes); + assertType('array', $a); + + $b = array_map(function ($b) { + assertType('array{foo: string, bar: int}', $b); + + return $b['foo']; + }, $this->arrayShapes); + assertType('array', $b); + } + + public function doBar(): void + { + usort($this->arrayShapes, function (array $a, array $b): int { + assertType('array{foo: string, bar: int}', $a); + assertType('array{foo: string, bar: int}', $b); + + return 1; + }); + } + + public function doBaz(): void + { + usort($this->arrayShapes, function ($a, $b): int { + assertType('array{foo: string, bar: int}', $a); + assertType('array{foo: string, bar: int}', $b); + + return 1; + }); + } + + public function closureNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (function () { + assertType('DateTimeInterface&stdClass', $this); + })->call($foo); + } + + public function arrowFunctionNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (fn () => assertType('DateTimeInterface&stdClass', $this))->call($foo); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/collected-data.php b/tests/PHPStan/Analyser/nsrt/collected-data.php new file mode 100644 index 0000000000..10c6d5fd8c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/collected-data.php @@ -0,0 +1,36 @@ + + */ +class TestCollector implements Collector +{ + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope) + { + return 1; + } + +} + +class Foo +{ + + public function doFoo(CollectedDataNode $node): void + { + assertType('array>', $node->get(TestCollector::class)); + } + +} diff --git a/tests/PHPStan/Analyser/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/data/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php similarity index 100% rename from tests/PHPStan/Analyser/data/comparison-operators.php rename to tests/PHPStan/Analyser/nsrt/comparison-operators.php diff --git a/tests/PHPStan/Analyser/data/complex-generics-example.php b/tests/PHPStan/Analyser/nsrt/complex-generics-example.php similarity index 100% rename from tests/PHPStan/Analyser/data/complex-generics-example.php rename to tests/PHPStan/Analyser/nsrt/complex-generics-example.php diff --git a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php new file mode 100644 index 0000000000..0873226897 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php @@ -0,0 +1,64 @@ +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)) { + $this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given'; + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + continue; + } + assertType("mixed", $this->config['authors']); + foreach (['homepage', 'email', 'name', 'role'] as $authorData) { + if (isset($author[$authorData]) && !is_string($author[$authorData])) { + $this->errors[] = 'authors.'.$key.'.'.$authorData.' : invalid value, must be a string'; + unset($this->config['authors'][$key][$authorData]); + } + } + if (isset($author['homepage'])) { + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]['homepage']); + assertType("mixed", $this->config['authors']); + } + if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { + unset($this->config['authors'][$key]['email']); + } + if (empty($this->config['authors'][$key])) { + unset($this->config['authors'][$key]); + } + } + + assertType("non-empty-array&hasOffsetValue('authors', mixed)", $this->config); + assertType("mixed", $this->config['authors']); + + if (empty($this->config['authors'])) { + unset($this->config['authors']); + assertType("array", $this->config); + } else { + assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); + } + + assertType("array", $this->config); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php new file mode 100644 index 0000000000..0cec47c79a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php @@ -0,0 +1,46 @@ +config['authors'])) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); + foreach ($this->config['authors'] as $key => $author) { + assertType("mixed", $this->config['authors']); + if (!is_array($author)) { + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + continue; + } + foreach (['homepage', 'email', 'name', 'role'] as $authorData) { + if (isset($author[$authorData]) && !is_string($author[$authorData])) { + unset($this->config['authors'][$key][$authorData]); + } + } + if (isset($author['homepage'])) { + unset($this->config['authors'][$key]['homepage']); + } + if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { + unset($this->config['authors'][$key]['email']); + } + if (empty($this->config['authors'][$key])) { + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + } + assertType("mixed", $this->config['authors']); + } + assertType("mixed", $this->config['authors']); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php b/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php new file mode 100644 index 0000000000..4754ea1db4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php @@ -0,0 +1,42 @@ + $files + */ + function setupDummyRepo(array $files): void + { + assertType('array', $files); + assertNativeType('array', $files); + foreach ($files as $path => $content) { + assertType('non-empty-array', $files); + assertNativeType('non-empty-array', $files); + assertType('string', $path); + assertNativeType('(int|string)', $path); + assertType('string|null', $content); + assertNativeType('mixed', $content); + assertType('string|null', $files[$path]); + assertNativeType('mixed', $files[$path]); + if ($files[$path] === null) { + assertType('null', $files[$path]); + assertNativeType('null', $files[$path]); + $files[$path] = 'content'; + assertType('\'content\'', $files[$path]); + assertNativeType('\'content\'', $files[$path]); + } + + assertType('string', $files[$path]); + assertNativeType('mixed~null', $files[$path]); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php similarity index 88% rename from tests/PHPStan/Analyser/data/conditional-non-empty-array.php rename to tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php index e13d22e3a0..7f3ede3f30 100644 --- a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php @@ -19,10 +19,10 @@ public function doFoo(array $a): void assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); if (count($a) > 0) { - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); assertVariableCertainty(TrinaryLogic::createYes(), $foo); } else { - assertType('array()', $a); + assertType('array{}', $a); assertVariableCertainty(TrinaryLogic::createNo(), $foo); } } diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php b/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php new file mode 100644 index 0000000000..2d19a65439 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php @@ -0,0 +1,24 @@ +returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_NO_EMPTY)); + assertType('true', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(1)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_OFFSET_CAPTURE)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(4)); + assertType('bool', $this->returnsTrueForPREG_SPLIT_NO_EMPTY($_GET['flag'])); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php new file mode 100644 index 0000000000..596e97634a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php @@ -0,0 +1,93 @@ += 8.0 + +namespace ConditionalTypesInference; + + +use function PHPStan\Testing\assertType; + +/** + * @return ($value is int ? true : false) + */ +function testIsInt(mixed $value): bool +{ + return is_int($value); +} + +/** + * @return ($value is not int ? true : false) + */ +function testIsNotInt(mixed $value): bool +{ + return !is_int($value); +} + +/** + * @return ($value is int ? void : never) + */ +function assertIsInt(mixed $value): void { + assert(is_int($value)); +} + +function (mixed $value) { + if (testIsInt($value)) { + assertType('int', $value); + } else { + assertType('mixed~int', $value); + } + + if (testIsNotInt($value)) { + assertType('mixed~int', $value); + } else { + assertType('int', $value); + } + + assertIsInt($value); + assertType('int', $value); +}; + +function (string $value) { + if (testIsInt($value)) { + assertType('*NEVER*', $value); + } else { + assertType('string', $value); + } + + if (testIsNotInt($value)) { + assertType('string', $value); + } else { + assertType('*NEVER*', $value); + } + + assertIsInt($value); + assertType('*NEVER*', $value); +}; + +function (int $value) { + if (testIsInt($value)) { + assertType('int', $value); + } else { + assertType('*NEVER*', $value); + } + + if (testIsNotInt($value)) { + assertType('*NEVER*', $value); + } else { + assertType('int', $value); + } + + assertIsInt($value); + assertType('int', $value); +}; + +/** + * @return ($condition is true ? void : never) + */ +function invariant(bool $condition, string $message): void +{ + assert($condition, $message); +} + +function (mixed $value) { + invariant(is_array($value), 'must be array'); + assertType('array', $value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types.php b/tests/PHPStan/Analyser/nsrt/conditional-types.php new file mode 100644 index 0000000000..0cdc741503 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types.php @@ -0,0 +1,252 @@ + + * + * @param TArray $array + * + * @return (TArray is non-empty-array ? non-empty-list : list) + */ + abstract public function arrayKeys(array $array); + + /** + * @param array $array + * @param non-empty-array $nonEmptyArray + * + * @param array $intArray + * @param non-empty-array $nonEmptyIntArray + * + * @param array{} $emptyArray + */ + public function testArrayKeys(array $array, array $nonEmptyArray, array $intArray, array $nonEmptyIntArray, array $emptyArray): void + { + assertType('list<(int|string)>', $this->arrayKeys($array)); + assertType('list', $this->arrayKeys($intArray)); + + assertType('non-empty-list<(int|string)>', $this->arrayKeys($nonEmptyArray)); + assertType('non-empty-list', $this->arrayKeys($nonEmptyIntArray)); + + assertType('list<*NEVER*>', $this->arrayKeys($emptyArray)); + } + + /** + * @return ($array is non-empty-array ? true : false) + */ + abstract public function accessory(array $array): bool; + + /** + * @param array $array + * @param non-empty-array $nonEmptyArray + * @param array{} $emptyArray + */ + public function testAccessory(array $array, array $nonEmptyArray, array $emptyArray): void + { + assertType('bool', $this->accessory($array)); + assertType('true', $this->accessory($nonEmptyArray)); + assertType('false', $this->accessory($emptyArray)); + assertType('bool', $this->accessory($_GET['array'])); + } + + /** + * @return ($as_float is true ? float : string) + */ + abstract public function microtime(bool $as_float); + + public function testMicrotime(): void + { + assertType('float', $this->microtime(true)); + assertType('string', $this->microtime(false)); + + assertType('float|string', $this->microtime($_GET['as_float'])); + } + + /** + * @return ($version is 8 ? true : ($version is 10 ? true : false)) + */ + abstract public function versionIsEightOrTen(int $version); + + public function testVersionIsEightOrTen(): void + { + assertType('false', $this->versionIsEightOrTen(6)); + assertType('false', $this->versionIsEightOrTen(7)); + assertType('true', $this->versionIsEightOrTen(8)); + assertType('false', $this->versionIsEightOrTen(9)); + assertType('true', $this->versionIsEightOrTen(10)); + assertType('false', $this->versionIsEightOrTen(11)); + assertType('false', $this->versionIsEightOrTen(12)); + + assertType('bool', $this->versionIsEightOrTen($_GET['version'])); + } + + /** + * @return ($parameter is true ? int : string) + */ + abstract public function missingParameter(); + + public function testMissingParameter(): void + { + assertType('int|string', $this->missingParameter()); + } + + /** + * @return (5 is int ? true : false) + */ + abstract public function deterministicReturnValue(); + + public function testDeterministicReturnValue(): void + { + assertType('true', $this->deterministicReturnValue()); + } + + /** + * @param (true is true ? string : bool) $foo + * @param (5 is int<4, 6> ? string : bool) $bar + * @param (5 is not int<0, 4> ? (4 is bool ? float : string) : bool) $baz + */ + public function testDeterministicParameter($foo, $bar, $baz): void + { + assertType('string', $foo); + assertType('string', $bar); + assertType('string', $baz); + } + + /** + * @template TInt of int + * @param TInt $foo + * @param (TInt is 5 ? int<0, 10> : int<10, 100>) $bar + */ + public function testConditionalInParameter(int $foo, int $bar): void + { + assertType('TInt of int (method ConditionalTypes\Test::testConditionalInParameter(), argument)', $foo); + assertType('int<0, 100>', $bar); + } + + /** + * @return ($input is null ? null : string) + */ + abstract public function retainNullable(?bool $input): ?string; + + public function testRetainNullable(?bool $input): void + { + assertType('string|null', $this->retainNullable($input)); + + if ($input === null) { + assertType('null', $this->retainNullable($input)); + } else { + assertType('string', $this->retainNullable($input)); + } + } + + /** + * @return ($option is 1 ? never : void) + */ + abstract public function maybeNever(int $option): void; + + public function testMaybeNever(): void + { + assertType('null', $this->maybeNever(0)); + assertType('never', $this->maybeNever(1)); + assertType('null', $this->maybeNever(2)); + } + + /** + * @return ($if is true ? mixed : null)|false + */ + abstract public function lateConditional1(bool $if); + + /** + * @return ($if is true ? mixed : null)|($if is true ? null : mixed)|false + */ + abstract public function lateConditional2(bool $if); + + public function testLateConditional(): void + { + assertType('mixed', $this->lateConditional1(true)); + assertType('false|null', $this->lateConditional1(false)); + + assertType('mixed', $this->lateConditional2(true)); + assertType('mixed', $this->lateConditional2(false)); + } +} + +class ParentClassToInherit +{ + + /** + * @param mixed $p + * @return ($p is int ? int : string) + */ + public function doFoo($p) + { + + } + +} + +class ChildClass extends ParentClassToInherit +{ + + public function doFoo($p) + { + + } + +} + +function (ChildClass $c): void { + assertType('int', $c->doFoo(1)); + assertType('string', $c->doFoo('foo')); +}; + +class ChildClass2 extends ParentClassToInherit +{ + + public function doFoo($x) + { + + } + +} + +function (ChildClass2 $c): void { + assertType('int', $c->doFoo(1)); + assertType('string', $c->doFoo('foo')); +}; + +/** + * @template T of object + */ +class ConditionalTypeFromClassScopeGenerics +{ + + /** + * @return (T is \Exception ? string : int) + */ + public function doFoo() + { + + } + +} + +class TestConditionalTypeFromClassScopeGenerics +{ + + /** + * @param ConditionalTypeFromClassScopeGenerics<\Exception> $a + * @param ConditionalTypeFromClassScopeGenerics<\stdClass> $b + */ + public function doFoo(ConditionalTypeFromClassScopeGenerics $a, ConditionalTypeFromClassScopeGenerics $b) + { + assertType('string', $a->doFoo()); + assertType('int', $b->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php new file mode 100644 index 0000000000..568c6a8b7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php @@ -0,0 +1,38 @@ + $innerHits */ + public function conditionalVarInTernary(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('non-empty-array', $innerHits); + $x = array_key_exists('nearest_premise', $innerHits) + ? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits) + : assertType('non-empty-array', $innerHits); + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } + + /** @param array $innerHits */ + public function conditionalVarInIf(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('non-empty-array', $innerHits); + if (array_key_exists('nearest_premise', $innerHits)) { + assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits); + } else { + assertType('non-empty-array', $innerHits); + } + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } +} diff --git a/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php rename to tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions-namespaced.php b/tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions-namespaced.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions.php b/tests/PHPStan/Analyser/nsrt/const-in-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions.php diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php b/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php new file mode 100644 index 0000000000..c1633bb14a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php @@ -0,0 +1,17 @@ + $array1 + * @param array&array{key: string|null} $array2 + */ +function test( + array $array1, + array $array2, +): void { + assertType('array{key: string}', $array1); + assertType('array{key: string}', $array2); +} diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php new file mode 100644 index 0000000000..fe3512a45b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php @@ -0,0 +1,114 @@ +|int $nextAutoIndexes + * @return void + */ + public function doFoo($nextAutoIndexes) + { + assertType('int|non-empty-list', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + assertType('int', $nextAutoIndexes); + } else { + assertType('non-empty-list', $nextAutoIndexes); + } + assertType('int|non-empty-list', $nextAutoIndexes); + } + + /** + * @param non-empty-list|int $nextAutoIndexes + * @return void + */ + public function doBar($nextAutoIndexes) + { + assertType('int|non-empty-list', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + $nextAutoIndexes = [$nextAutoIndexes]; + assertType('array{int}', $nextAutoIndexes); + } else { + assertType('non-empty-list', $nextAutoIndexes); + } + assertType('non-empty-list', $nextAutoIndexes); + } + +} + +class Baz +{ + + public function doFoo() + { + $conditionalArray = [1, 1, 1]; + if (doFoo()) { + $conditionalArray[] = 2; + $conditionalArray[] = 3; + } + + assertType('array{1, 1, 1, 2, 3}|array{1, 1, 1}', $conditionalArray); + + $unshiftedConditionalArray = $conditionalArray; + array_unshift($unshiftedConditionalArray, 'lorem', new \stdClass()); + assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray); + + assertType('array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}', $conditionalArray + $unshiftedConditionalArray); + assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray + $conditionalArray); + + $conditionalArray[] = 4; + assertType('array{1, 1, 1, 2, 3, 4}|array{1, 1, 1, 4}', $conditionalArray); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php new file mode 100644 index 0000000000..69bfb7a1a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php @@ -0,0 +1,80 @@ +', $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..4471f9c168 --- /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..1399228135 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/countable.php @@ -0,0 +1,52 @@ +', $foo->count()); + } +} + +class Bar implements \Countable { + + /** + * @return -1 + */ + public function count() : int { + return -1; + } + + static public function doBar() { + $bar = new Bar(); + assertType('-1', $bar->count()); + } +} + +interface Baz { +} + +class NonCountable {} + +function doNonCountable() { + assertType('*ERROR*', count(new NonCountable())); +} + +function doFoo() { + assertType('int<0, max>', count(new Foo())); +} + +function doBar() { + assertType('-1', count(new Bar())); +} + +function doBaz(Baz $baz) { + assertType('int<0, max>', count($baz)); +} 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..94750ddacc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo.php @@ -0,0 +1,69 @@ +>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string, appconnect_time_us: int, queue_time_us: int, connect_time_us: int, namelookup_time_us: int, pretransfer_time_us: int, redirect_time_us: int, starttransfer_time_us: int, posttransfer_time_us: int, total_time_us: int, request_header: string, effective_method: string, capath: string, cainfo: string, used_proxy: int, httpauth_used: int, proxyauth_used: int, conn_id: int}|false)'; + + $handle = new CurlHandle(); + assertType('mixed', curl_getinfo()); + assertType('mixed', CURL_GETINFO()); + assertType('mixed', CuRl_GeTiNfO()); + assertType('false', curl_getinfo($handle, 'Invalid Argument')); + assertType($curlGetInfoType, curl_getinfo($handle, PHP_INT_MAX)); + assertType('false', curl_getinfo($handle, PHP_EOL)); + assertType($curlGetInfoType, curl_getinfo($handle)); + assertType($curlGetInfoType, 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|false', 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('float', curl_getinfo($handle, CURLINFO_SIZE_UPLOAD)); + assertType('float', curl_getinfo($handle, CURLINFO_SIZE_DOWNLOAD)); + assertType('float', curl_getinfo($handle, CURLINFO_SPEED_DOWNLOAD)); + assertType('float', 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('mixed', curl_getinfo($handle, CURLINFO_PRIVATE)); + assertType('int', curl_getinfo($handle, CURLINFO_RESPONSE_CODE)); + assertType('int', curl_getinfo($handle, CURLINFO_HTTP_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('list', curl_getinfo($handle, CURLINFO_SSL_ENGINES)); + assertType('list', curl_getinfo($handle, CURLINFO_COOKIELIST)); + assertType('string|false', curl_getinfo($handle, CURLINFO_FTP_ENTRY_PATH)); + assertType('float', curl_getinfo($handle, CURLINFO_APPCONNECT_TIME)); + assertType('list>', 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('string|false', 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..90e7c82e83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php @@ -0,0 +1,32 @@ += 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('int', 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/curl_getinfo_8.2.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.2.php new file mode 100644 index 0000000000..07cd3431da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.2.php @@ -0,0 +1,18 @@ += 8.2 + +namespace CurlGetinfo82; + +use CurlHandle; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function bar() + { + $handle = new CurlHandle(); + assertType('string', curl_getinfo($handle, CURLINFO_EFFECTIVE_METHOD)); + assertType('int', curl_getinfo($handle, CURLINFO_PROXY_ERROR)); + assertType('string|false', curl_getinfo($handle, CURLINFO_REFERER)); + assertType('int', curl_getinfo($handle, CURLINFO_RETRY_AFTER)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.3.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.3.php new file mode 100644 index 0000000000..5d9d4258cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.3.php @@ -0,0 +1,16 @@ += 8.3 + +namespace CurlGetinfo83; + +use CurlHandle; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function bar() + { + $handle = new CurlHandle(); + assertType('string|false', curl_getinfo($handle, CURLINFO_CAINFO)); + assertType('string|false', curl_getinfo($handle, CURLINFO_CAPATH)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.4.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.4.php new file mode 100644 index 0000000000..ca896481d0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_8.4.php @@ -0,0 +1,15 @@ += 8.4 + +namespace CurlGetinfo84; + +use CurlHandle; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function bar() + { + $handle = new CurlHandle(); + assertType('int|false', curl_getinfo($handle, CURLINFO_POSTTRANSFER_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-interval-format.php b/tests/PHPStan/Analyser/nsrt/date-interval-format.php new file mode 100644 index 0000000000..691d9e3328 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date-interval-format.php @@ -0,0 +1,40 @@ +format($string)); + assertType('non-empty-string', $dateInterval->format($nonEmptyString)); + + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format('%Y')); // '00' + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format('%y')); // '0' + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format($unionString1)); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format($unionString2)); + + assertType('non-falsy-string&uppercase-string', $dateInterval->format('%Y DAYS')); + assertType('non-falsy-string&uppercase-string', $dateInterval->format($unionString1. ' DAYS')); + + assertType('lowercase-string&non-falsy-string', $dateInterval->format('%Y days')); + assertType('lowercase-string&non-falsy-string', $dateInterval->format($unionString1. ' days')); + } +} 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..e6c4cd790a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date-period-return-types.php @@ -0,0 +1,56 @@ +', $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()); + +class Foo +{ + private DatePeriod $period; + + public function doFoo(DateTimeImmutable $fromDate, DateTimeImmutable $toDate): void + { + $this->period = new DatePeriod($fromDate, new DateInterval('P1D'), $toDate->modify('+1 day')); + } +} 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 @@ +', $itemsCounter); } - assertType('Generator&iterable', $associationData); + assertType('Generator', $associationData); - assertType('int', $itemsCounter); + assertType('int<0, max>', $itemsCounter); } } diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php new file mode 100644 index 0000000000..d59a186bf0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Discussion10285Php8; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(): void + { + $socket = socket_create(AF_INET, SOCK_STREAM, 0); + if($socket === false) return; + $read = [$socket]; + $write = []; + $except = null; + socket_select($read, $write, $except, 0, 1); + assertType('array', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285.php b/tests/PHPStan/Analyser/nsrt/discussion-10285.php new file mode 100644 index 0000000000..4b8da269ad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285.php @@ -0,0 +1,21 @@ +', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-13395.php b/tests/PHPStan/Analyser/nsrt/discussion-13395.php new file mode 100644 index 0000000000..0a48988dde --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-13395.php @@ -0,0 +1,86 @@ + $rows + * @return non-empty-list|null + */ + public function fetch( + array $rows, + ) : ?array { + return $this->buildViews($rows); + } + + /** + * @param non-empty-list $rows + * @return non-empty-list|null + */ + private function buildViews(array $rows) : ?array + { + if ($rows[0]['value'] === null) { + return null; + } + + $views = \array_map( + static function (array $row) : View { + \assert($row['value'] !== null); + return new View( + $row['value'], + ); + }, + $rows, + ); + + assertType('non-empty-list&hasOffsetValue(0, Discussion13395\View)', $views); // could be just non-empty-list + + return $views; + } + + /** + * @param non-empty-list $rows + * @return non-empty-list|null + */ + private function buildViews2(array $rows) : ?array + { + if ($rows[0]['value'] === null) { + return null; + } + + if ($rows[1]['value'] === null) { + return null; + } + + $views = \array_map( + static function (array $row) : View { + \assert($row['value'] !== null); + return new View( + $row['value'], + ); + }, + $rows, + ); + + assertType('non-empty-list&hasOffsetValue(0, Discussion13395\View)&hasOffsetValue(1, Discussion13395\View)', $views); // could be just &hasOffsetValue(1, Discussion13395\View) + + return $views; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-8447.php b/tests/PHPStan/Analyser/nsrt/discussion-8447.php new file mode 100644 index 0000000000..21bd7d4dda --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-8447.php @@ -0,0 +1,45 @@ + + * @param TLead $lead + * @return TQuote + */ + public function store(Lead $lead): Quote + { + assertType('TQuote of Discussion8447\Quote (class Discussion8447\Controller, argument)', $lead->quoteRepository()->create()); + return $lead->quoteRepository()->create(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-9134.php b/tests/PHPStan/Analyser/nsrt/discussion-9134.php new file mode 100644 index 0000000000..330b51cbaa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-9134.php @@ -0,0 +1,12 @@ +|false', $res); +if (is_array($res) === false) { + throw new \RuntimeException(); +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-9972.php b/tests/PHPStan/Analyser/nsrt/discussion-9972.php new file mode 100644 index 0000000000..c0e4eabf23 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-9972.php @@ -0,0 +1,26 @@ +helper($myBool); + + if ($myBool) { + assertVariableCertainty(TrinaryLogic::createYes(), $myObject); + } + } + + protected function helper(bool $input): void + { + } +} diff --git a/tests/PHPStan/Analyser/nsrt/div-by-zero.php b/tests/PHPStan/Analyser/nsrt/div-by-zero.php new file mode 100644 index 0000000000..ad027888bc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/div-by-zero.php @@ -0,0 +1,28 @@ + $range1 + * @param int $range2 + */ + public function doFoo(int $range1, int $range2, int $int): void + { + assertType('float|int<1, 5>', 5 / $range1); + assertType('float|int<-5, -1>', 5 / $range2); + assertType('float|int', $range1 / $range2); + assertType('(float|int)', 5 / $int); + + assertType('*ERROR*', 5 / 0); + assertType('*ERROR*', 5 / '0'); + assertType('*ERROR*', 5 / 0.0); + assertType('*ERROR*', 5 / false); + assertType('*ERROR*', 5 / null); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/dnf.php b/tests/PHPStan/Analyser/nsrt/dnf.php new file mode 100644 index 0000000000..dbe8d11eef --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dnf.php @@ -0,0 +1,25 @@ += 8.2 + +namespace Dnf; + +use function PHPStan\Testing\assertType; + +interface A {} +interface B {} +interface D {} + +class Foo +{ + + public function doFoo((A&B)|D $a): void + { + assertType('(Dnf\A&Dnf\B)|Dnf\D', $a); + assertType('(Dnf\A&Dnf\B)|Dnf\D', $this->doBar()); + } + + public function doBar(): (A&B)|D + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php new file mode 100644 index 0000000000..6900709f99 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php @@ -0,0 +1,123 @@ +', rand(0, 1)); + } + }; + + function (): void { + if (rand(0, 1) === 0) { + assertType('int<0, 1>', rand(0, 1)); + } + }; + function (): void { + assertType('1|\'foo\'', rand(0, 1) ?: 'foo'); + assertType('\'foo\'|int<0, 1>', rand(0, 1) ? rand(0, 1) : 'foo'); + }; + } + + public function doBar(): bool + { + + } + + /** @phpstan-pure */ + public function doBaz(): bool + { + + } + + /** @phpstan-impure */ + public function doLorem(): bool + { + + } + + public function doIpsum() + { + if ($this->doBar() === true) { + assertType('true', $this->doBar()); + } + + if ($this->doBaz() === true) { + assertType('true', $this->doBaz()); + } + + if ($this->doLorem() === true) { + assertType('bool', $this->doLorem()); + } + } + + public function doDolor() + { + if ($this->doBar()) { + assertType('true', $this->doBar()); + } + + if ($this->doBaz()) { + assertType('true', $this->doBaz()); + } + + if ($this->doLorem()) { + assertType('bool', $this->doLorem()); + } + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/ds-copy.php b/tests/PHPStan/Analyser/nsrt/ds-copy.php new file mode 100644 index 0000000000..8bf29a71ba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ds-copy.php @@ -0,0 +1,65 @@ + $col + * @param Sequence $seq + * @param Vector $vec + * @param Deque $deque + * @param Map $map + * @param Queue $queue + * @param Stack $stack + * @param PriorityQueue $pq + * @param Set $set + */ + public function __construct( + private readonly Collection $col, + private readonly Sequence $seq, + private readonly Vector $vec, + private readonly Deque $deque, + private readonly Map $map, + private readonly Queue $queue, + private readonly Stack $stack, + private readonly PriorityQueue $pq, + private readonly Set $set, + ) { + } + + public function copy(): void + { + $col = $this->col->copy(); + $seq = $this->seq->copy(); + $vec = $this->vec->copy(); + $deque = $this->deque->copy(); + $map = $this->map->copy(); + $queue = $this->queue->copy(); + $stack = $this->stack->copy(); + $pq = $this->pq->copy(); + $set = $this->set->copy(); + + assertType('Ds\Collection', $col); + assertType('Ds\Sequence', $seq); + assertType('Ds\Vector', $vec); + assertType('Ds\Deque', $deque); + assertType('Ds\Map', $map); + assertType('Ds\Queue', $queue); + assertType('Ds\Stack', $stack); + assertType('Ds\PriorityQueue', $pq); + assertType('Ds\Set', $set); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php new file mode 100644 index 0000000000..3555613fe0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -0,0 +1,39 @@ + $a + * @param 'b'|'bb' $b + */ + public function integerRange(int $a, string $b): void + { + assertType("'0 b'|'0 bb'|'1 b'|'1 bb'|'2 b'|'2 bb'|'3 b'|'3 bb'", sprintf('%d %s', $a, $b)); + } + + /** + * @param int<0,64> $a + * @param 'b'|'bb' $b + */ + public function tooBigRange(int $a, string $b): void + { + assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b)); + } + +} diff --git a/tests/PHPStan/Analyser/data/early-termination-phpdoc.php b/tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/early-termination-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php diff --git a/tests/PHPStan/Analyser/data/empty-array-shape.php b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php similarity index 83% rename from tests/PHPStan/Analyser/data/empty-array-shape.php rename to tests/PHPStan/Analyser/nsrt/empty-array-shape.php index 4a19bf0f59..35d3dcece9 100644 --- a/tests/PHPStan/Analyser/data/empty-array-shape.php +++ b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php @@ -10,7 +10,7 @@ class Foo /** @param array{} $array */ public function doFoo(array $array): void { - assertType('array()', $array); + assertType('array{}', $array); } } diff --git a/tests/PHPStan/Analyser/nsrt/emptyiterator.php b/tests/PHPStan/Analyser/nsrt/emptyiterator.php new file mode 100644 index 0000000000..fd338eda7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/emptyiterator.php @@ -0,0 +1,18 @@ +key()); + assertType('never', $it->current()); + assertType('null', $it->next()); + assertType('false', $it->valid()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-from.php b/tests/PHPStan/Analyser/nsrt/enum-from.php new file mode 100644 index 0000000000..9f65726914 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-from.php @@ -0,0 +1,94 @@ += 8.1 + +declare(strict_types=1); + +namespace EnumFrom; + +use function PHPStan\Testing\assertType; + +enum FooIntegerEnum: int +{ + + case BAR = 1; + case BAZ = 2; + +} + +enum FooIntegerEnumSubset: int +{ + + case BAR = 1; + +} + +enum FooStringEnum: string +{ + + case BAR = 'bar'; + case BAZ = 'baz'; + +} + +enum FooNumericStringEnum: string +{ + + case ONE = '1'; + +} + +class Foo +{ + + public function doFoo(): void + { + assertType('1', FooIntegerEnum::BAR->value); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::BAR); + + assertType('null', FooIntegerEnum::tryFrom(0)); + assertType(FooIntegerEnum::class, FooIntegerEnum::from(0)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom(0 + 1)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from(1 * FooIntegerEnum::BAR->value)); + + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(2)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(FooIntegerEnum::BAZ->value)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::from(FooIntegerEnum::BAZ->value)); + + assertType("'bar'", FooStringEnum::BAR->value); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::BAR); + + assertType('null', FooStringEnum::tryFrom('barz')); + assertType(FooStringEnum::class, FooStringEnum::from('barz')); + + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::tryFrom('ba' . 'r')); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::from(sprintf('%s%s', 'ba', 'r'))); + + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom('baz')); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom(FooStringEnum::BAZ->value)); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::from(FooStringEnum::BAZ->value)); + + assertType('null', FooIntegerEnum::tryFrom('1')); + assertType('null', FooIntegerEnum::tryFrom(1.0)); + assertType('null', FooIntegerEnum::tryFrom(1.0001)); + assertType('null', FooIntegerEnum::tryFrom(true)); + assertType('null', FooNumericStringEnum::tryFrom(1)); + } + + public function supersetToSubset(FooIntegerEnum $foo): void + { + assertType('EnumFrom\FooIntegerEnumSubset::BAR|null', FooIntegerEnumSubset::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnumSubset::BAR', FooIntegerEnumSubset::from($foo->value)); + } + + public function subsetToSuperset(FooIntegerEnumSubset $foo): void + { + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from($foo->value)); + } + + public function doCaseInsensitive(): void + { + assertType('1', FooInTeGerEnum::BAR->value); + assertType('null', FooInTeGerEnum::tryFrom(0)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-in-array.php new file mode 100644 index 0000000000..ca34261bc8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-in-array.php @@ -0,0 +1,187 @@ += 8.1 + +use function PHPStan\Testing\assertType; + +enum MyEnum: string +{ + + case A = 'a'; + case B = 'b'; + case C = 'c'; + + const SET_AB = [self::A, self::B]; + const SET_C = [self::C]; + const SET_ABC = [self::A, self::B, self::C]; + + public function test1(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::A|MyEnum::B', $enum); + } elseif (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test2(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_ABC, true)) { + assertType('MyEnum::A|MyEnum::B|MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test3(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function test4(): void + { + foreach ([MyEnum::C] as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative1(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function testNegative2(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } elseif (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative3(): void + { + foreach ([MyEnum::C] as $enum) { + if (!in_array($enum, MyEnum::SET_C, true)) { + assertType('*NEVER*', $enum); + } + } + } + + /** + * @param array $array + */ + public function testNegative4(MyEnum $enum, array $array): void + { + if (!in_array($enum, $array, true)) { + assertType('MyEnum', $enum); + assertType('array', $array); + } else { + assertType('MyEnum', $enum); + assertType('non-empty-array', $array); + } + } + +} + +class InArrayEnum +{ + + /** @param list $list */ + public function testPositive(MyEnum $enum, array $list): void + { + if (in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('list', $list); + } + + /** @param list $list */ + public function testNegative(MyEnum $enum, array $list): void + { + if (!in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('non-empty-list', $list); + } + +} + + +class InArrayOtherFiniteType { + + const SET_AB = ['a', 'b']; + const SET_C = ['c']; + const SET_ABC = ['a', 'b', 'c']; + + public function test1(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_AB, true)) { + assertType("'a'|'b'", $item); + } elseif (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test2(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_ABC, true)) { + assertType("'a'|'b'|'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test3(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType("'a'|'b'", $item); + } + } + } + public function test4(): void + { + foreach (['c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php new file mode 100644 index 0000000000..7584e5b4cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php @@ -0,0 +1,23 @@ += 8.2 + +namespace EnumReflection82; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionNamedType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionNamedType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-reflection.php b/tests/PHPStan/Analyser/nsrt/enum-reflection.php new file mode 100644 index 0000000000..85e98f4f8d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection.php @@ -0,0 +1,43 @@ += 8.1 + +namespace EnumReflection; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumBackedCase::class, $case); + } + + assertType(ReflectionEnumBackedCase::class, $r->getCase('FOO')); + } + +} + +enum Bar +{ + + case FOO; + case BAR; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumUnitCase::class, $case); + } + assertType(ReflectionEnumUnitCase::class, $r->getCase('FOO')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php new file mode 100644 index 0000000000..4a9a22e4c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php @@ -0,0 +1,43 @@ += 8.1 + +declare(strict_types = 1); + +namespace EnumVsInArray; + +use function PHPStan\Testing\assertType; + +enum FooEnum +{ + case A; + case B; + case C; + case D; + case E; + case F; + case G; + case H; + case I; + case J; +} + +function foo(FooEnum $e): int +{ + if (in_array($e, [FooEnum::A, FooEnum::B, FooEnum::C], true)) { + throw new \Exception('a'); + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + if (rand(0, 10) === 1) { + if (!in_array($e, [FooEnum::D, FooEnum::E], true)) { + throw new \Exception('d'); + } + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + return match ($e) { + FooEnum::D, FooEnum::E, FooEnum::F, FooEnum::G, FooEnum::H, FooEnum::I => 2, + FooEnum::J => 3, + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/enum_exists.php b/tests/PHPStan/Analyser/nsrt/enum_exists.php new file mode 100644 index 0000000000..37809016ad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum_exists.php @@ -0,0 +1,28 @@ += 8.0 + +namespace EnumExists; + +use function PHPStan\Testing\assertType; + +function getEnumValue(string $enumFqcn, string $name): mixed { + if (enum_exists($enumFqcn)) { + assertType('class-string', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('string', $enumFqcn); + + return null; +} + +/** + * @param class-string $enumFqcn + */ +function getEnumValueFromClassString(string $enumFqcn, string $name): mixed { + if (enum_exists($enumFqcn)) { + assertType('class-string', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('class-string', $enumFqcn); + + return null; +} diff --git a/tests/PHPStan/Analyser/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/data/eval-implicit-throw.php b/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/eval-implicit-throw.php rename to tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php diff --git a/tests/PHPStan/Analyser/nsrt/explicit-throws.php b/tests/PHPStan/Analyser/nsrt/explicit-throws.php new file mode 100644 index 0000000000..e37d6be94a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/explicit-throws.php @@ -0,0 +1,53 @@ +throwInvalidArgument(); + } catch (\InvalidArgumentException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + } + + public function doBaz(): void + { + try { + doFoo(); + $a = 1; + $this->throwInvalidArgument(); + throw new \InvalidArgumentException(); + } catch (\InvalidArgumentException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + } + + /** + * @throws \InvalidArgumentException + */ + private function throwInvalidArgument(): void + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/ext-ds.php b/tests/PHPStan/Analyser/nsrt/ext-ds.php similarity index 100% rename from tests/PHPStan/Analyser/data/ext-ds.php rename to tests/PHPStan/Analyser/nsrt/ext-ds.php diff --git a/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php new file mode 100644 index 0000000000..98c40a4326 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php @@ -0,0 +1,23 @@ +', $nonPositiveInt); + assertType('int<0, max>', $nonNegativeInt); + } + +} diff --git a/tests/PHPStan/Analyser/data/extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-int-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/extra-int-types.php rename to tests/PHPStan/Analyser/nsrt/extra-int-types.php diff --git a/tests/PHPStan/Analyser/nsrt/extract.php b/tests/PHPStan/Analyser/nsrt/extract.php new file mode 100644 index 0000000000..dff42bc482 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/extract.php @@ -0,0 +1,86 @@ + 42]); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} + + +function doTyped5(): void +{ + $foo = ['foo' => 42]; + extract($foo); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php b/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php new file mode 100644 index 0000000000..60cbd4971a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php @@ -0,0 +1,127 @@ +get('position'); + assertVariableCertainty(TrinaryLogic::createYes(), $location); + + $location ?? ''; + assertVariableCertainty(TrinaryLogic::createYes(), $location); + } + +} + +function maybeTrueVarAssign():void { + if (rand(0,1)) { + $a = true; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('true', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarAssign():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarAssign():void { + if (rand(0,1)) { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsAssign():void { + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarExpr():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarExpr():void { + if (rand(0,1)) { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsExpr():void { + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php new file mode 100644 index 0000000000..ba24b22730 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php @@ -0,0 +1,98 @@ + null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyEmptyUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a->x)) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function justEmpty(): void +{ + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } + assertVariableCertainty(TrinaryLogic::createNo(), $foo); +} + +function maybeEmpty(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + +function maybeEmptyUnset(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + unset($foo); + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + !empty($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php new file mode 100644 index 0000000000..484d0363e3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php @@ -0,0 +1,282 @@ += 8.0 + +namespace FalseyIssetCertainty; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function getFoo():mixed { + return 1; +} + +function falseyIssetArrayDimFetchOnProperty(): void +{ + $a = new \stdClass(); + $a->bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->x)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableUnset(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + unset($a); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createNo(), $a); +} + +function falseyIssetNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetWithAssignment(): void +{ + if (rand() % 2) { + $x = ['x' => 1]; + } + + if (isset($x[$z = getFoo()])) { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createYes(), $x); + + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); +} + +function justIsset(): void +{ + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } +} + +function maybeIsset(): void +{ + if (rand() % 2) { + $foo = 1; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('1', $foo); + } +} + +function isStringNarrowsMaybeCertainty(int $i, string $s): void +{ + if (rand(0, 1)) { + $a = rand(0,1) ? $i : $s; + } + + if (is_string($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + echo $a; + } +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php new file mode 100644 index 0000000000..cc831b87a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php @@ -0,0 +1,226 @@ += 8.0 + +namespace FalseyTernaryCertainty; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function getFoo():mixed { + return 1; +} + +function falseyTernaryArrayDimFetchOnProperty(): void +{ + $a = new \stdClass(); + $a->bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a->bar)? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->bar) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->x) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableShort(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ?: + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + return isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ? assertVariableCertainty(TrinaryLogic::createYes(), $matches) + : ''; +} diff --git a/tests/PHPStan/Analyser/nsrt/falsy-isset.php b/tests/PHPStan/Analyser/nsrt/falsy-isset.php new file mode 100644 index 0000000000..eb11c5254d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsy-isset.php @@ -0,0 +1,99 @@ += 8.0 + +namespace FalsyIsset; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function doFoo():mixed { + return 1; +} + +function maybeMixedVariable(): void +{ + if (rand(0,1)) { + $a = doFoo(); + } + + if (isset($a)) { + assertType("mixed~null", $a); + } else { + assertType("null", $a); + } +} + +function maybeNullableVariable(): void +{ + if (rand(0,1)) { + $a = 'hello'; + + if (rand(0,2)) { + $a = null; + } + } + + if (isset($a)) { + assertType("'hello'", $a); + } else { + assertType("null", $a); + } +} + +function subtractedMixedIsset(mixed $m): void +{ + if ($m === null) { + return; + } + + assertType("mixed~null", $m); + if (isset($m)) { + assertType("mixed~null", $m); + } else { + assertType("*ERROR*", $m); + } +} + +function mixedIsset(mixed $m): void +{ + if (isset($m)) { + assertType("mixed~null", $m); + } else { + assertType("null", $m); + } +} + +function stdclassIsset(?\stdClass $m): void +{ + if (isset($m)) { + assertType("stdClass", $m); + } else { + assertType("null", $m); + } +} + +function nullableVariable(?string $a): void +{ + if (isset($a)) { + assertType("string", $a); + } else { + assertType("null", $a); + } +} + +function nullableUnionVariable(null|string|int $a): void +{ + if (isset($a)) { + assertType("int|string", $a); + } else { + assertType("null", $a); + } +} + +function render(?int $noteListLimit, int $count): void +{ + $showAllLink = $noteListLimit !== null && $count > $noteListLimit; + if ($showAllLink) { + assertType('int', $noteListLimit); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php new file mode 100644 index 0000000000..be5f0eca91 --- /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..0cf0882023 --- /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('non-empty-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/data/foreach-dependent-key-value.php b/tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php similarity index 100% rename from tests/PHPStan/Analyser/data/foreach-dependent-key-value.php rename to tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php diff --git a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php new file mode 100644 index 0000000000..a1d7279252 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php @@ -0,0 +1,35 @@ +|false $a + */ + public function doFoo($a): void + { + foreach ($a as $k => $v) { + assertType('string', $k); + assertType('int', $v); + } + } + +} + +class Bar +{ + + public function sayHello(\stdClass $s): void + { + $a = null; + foreach ($s as $k => $v) { + $a .= 'test'; + } + assertType('(literal-string&lowercase-string&non-falsy-string)|null', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/fpm-get-status.php b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php new file mode 100644 index 0000000000..8a276ec7ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php @@ -0,0 +1,16 @@ += 7.3 + +namespace FpmGetStatus; + +use function fpm_get_status; +use function PHPStan\Testing\assertType; + +$status = fpm_get_status(); + +assertType('array{pool: string, process-manager: \'dynamic\'|\'ondemand\'|\'static\', start-time: int<0, max>, start-since: int<0, max>, accepted-conn: int<0, max>, listen-queue: int<0, max>, max-listen-queue: int<0, max>, listen-queue-len: int<0, max>, idle-processes: int<0, max>, active-processes: int<1, max>, total-processes: int<1, max>, max-active-processes: int<1, max>, max-children-reached: 0|1, slow-requests: int<0, max>, procs: array, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}>}|false', $status); + +if ($status !== false && isset($status['procs'][0])) { + assertType('array{pid: int<2, max>, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}', $status['procs'][0]); + + assertType('int<2, max>', $status['procs'][0]['pid']); +} diff --git a/tests/PHPStan/Analyser/nsrt/generic-callables.php b/tests/PHPStan/Analyser/nsrt/generic-callables.php new file mode 100644 index 0000000000..9fde822894 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-callables.php @@ -0,0 +1,80 @@ += 8.0 + +namespace GenericCallables; + +use Closure; + +use function PHPStan\Testing\assertType; + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return Closure(): TFuncRet + */ +function testFuncClosure(mixed $mixed): Closure +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return Closure(TClosureRet $val): (TClosureRet|TFuncRet) + */ +function testFuncClosureMixed(mixed $mixed) +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return callable(): TFuncRet + */ +function testFuncCallable(mixed $mixed): callable +{ +} + +/** + * @param Closure(TRet $val): TRet $callable + * @param non-empty-list(TRet $val): TRet> $callables + */ +function testClosure(Closure $callable, int $int, string $str, array $callables): void +{ + assertType('Closure(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('string', $callables[0]($str)); + assertType('Closure(): 1', testFuncClosure(1)); +} + +function testClosureMixed(int $int, string $str): void +{ + $closure = testFuncClosureMixed($int); + assertType('Closure(TClosureRet): (int|TClosureRet)', $closure); + assertType('int|string', $closure($str)); +} + +/** + * @param callable(TRet $val): TRet $callable + */ +function testCallable(callable $callable, int $int, string $str): void +{ + assertType('callable(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('callable(): 1', testFuncCallable(1)); +} + +/** + * @param Closure(TRetFirst $valone): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure + */ +function testNestedClosures(Closure $closure, string $str, int $int): void +{ + assertType('Closure(TRetFirst): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure); + $closure1 = $closure($str); + assertType('Closure(TRetSecond): (string|TRetSecond)', $closure1); + $result = $closure1($int); + assertType('int|string', $result); +} diff --git a/tests/PHPStan/Analyser/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/data/generic-object-lower-bound.php b/tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-object-lower-bound.php rename to tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php diff --git a/tests/PHPStan/Analyser/data/generic-offset-get.php b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php similarity index 84% rename from tests/PHPStan/Analyser/data/generic-offset-get.php rename to tests/PHPStan/Analyser/nsrt/generic-offset-get.php index 7243b96576..8f2cfa4f5a 100644 --- a/tests/PHPStan/Analyser/data/generic-offset-get.php +++ b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php @@ -9,6 +9,7 @@ class Foo implements ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { return true; @@ -19,16 +20,19 @@ public function offsetExists($offset) * @param class-string $offset * @return T */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { diff --git a/tests/PHPStan/Analyser/data/generic-parent.php b/tests/PHPStan/Analyser/nsrt/generic-parent.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-parent.php rename to tests/PHPStan/Analyser/nsrt/generic-parent.php diff --git a/tests/PHPStan/Analyser/nsrt/generic-static.php b/tests/PHPStan/Analyser/nsrt/generic-static.php new file mode 100644 index 0000000000..c7163151f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-static.php @@ -0,0 +1,173 @@ + + */ + public function map(callable $cb); + + /** @return static */ + public function flip(); + + /** @return static */ + public function fluent(); + + /** @return static> */ + public function nested(); + +} + +/** + * @template T + * @template U + * @implements Foo + */ +class FooImpl implements Foo +{ + + public function map(callable $cb) + { + + } + + public function flip() + { + + } + + public function fluent() + { + + } + + public function doFoo(): void + { + assertType('static(GenericStatic\FooImpl)', $this->map(function () { + return 1; + })); + + assertType('static(GenericStatic\FooImpl)', $this->flip()); + assertType('static(GenericStatic\FooImpl)', $this->fluent()); + assertType('static(GenericStatic\FooImpl)>)', $this->nested()); + } + + /** + * @param FooImpl $s + */ + public function doBar(self $s): void + { + assertType('GenericStatic\\FooImpl', $s->map(function () { + return 1; + })); + + assertType('GenericStatic\\FooImpl', $s->flip()); + assertType('GenericStatic\\FooImpl', $s->fluent()); + assertType('GenericStatic\FooImpl>', $s->nested()); + } + +} + +/** + * @template T + * @template U + * @implements Foo + */ +abstract class Inconsistent implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent)', $this->fluent()); + assertType('GenericStatic\\Inconsistent', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent2 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent2 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent2)', $this->fluent()); + assertType('GenericStatic\\Inconsistent2', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent3 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent3 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent3)', $this->fluent()); + assertType('GenericStatic\\Inconsistent3', $s->fluent()); + } + +} + +/** + * @template T + * @template K + */ +class A { + /** @return static */ + public function doFoo() {} + +} + +/** @extends A */ +class B extends A { + public function doBar(): void + { + $f = $this->doFoo(); + assertType('static(GenericStatic\B)', $f); + } +} + +function (): void { + assertType(B::class, (new B)->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/data/generic-traits.php b/tests/PHPStan/Analyser/nsrt/generic-traits.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-traits.php rename to tests/PHPStan/Analyser/nsrt/generic-traits.php diff --git a/tests/PHPStan/Analyser/nsrt/generic-unions.php b/tests/PHPStan/Analyser/nsrt/generic-unions.php new file mode 100644 index 0000000000..da3d1bac83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-unions.php @@ -0,0 +1,135 @@ +doFoo($nullableString)); + assertType('int|string', $this->doFoo($stringOrInt)); + + assertType('string|null', $this->doBar($nullableString)); + + assertType('1', $this->doBaz(1)); + assertType('\'foo\'', $this->doBaz('foo')); + assertType('1.2', $this->doBaz(1.2)); + assertType('string', $this->doBaz($stringOrInt)); + } + +} + +class InvokableClass +{ + public function __invoke(): string + { + return 'foo'; + } +} + +/** + * + * @template TGetDefault + * @template TKey + * + * @param TKey $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TKey|TGetDefault + */ +function getWithDefault($key, $default = null) +{ + if(rand(0,10) > 5) { + return $key; + } + + if (is_callable($default)) { + return $default(); + } + + return $default; +} + +/** + * + * @template TGetDefault + * @template TKey + * + * @param TKey $key + * @param TGetDefault|(callable(): TGetDefault) $default + * @return TKey|TGetDefault + */ +function getWithDefaultCallable($key, $default = null) +{ + if(rand(0,10) > 5) { + return $key; + } + + if (is_callable($default)) { + return $default(); + } + + return $default; +} + +assertType('3|null', getWithDefault(3)); +assertType('3|null', getWithDefaultCallable(3)); +assertType('3|\'foo\'', getWithDefault(3, 'foo')); +assertType('3|\'foo\'', getWithDefaultCallable(3, 'foo')); +assertType('3|\'foo\'', getWithDefault(3, function () { + return 'foo'; +})); +assertType('3|\'foo\'', getWithDefaultCallable(3, function () { + return 'foo'; +})); +assertType('3|GenericUnions\Foo', getWithDefault(3, function () { + return new Foo; +})); +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, function () { + return new Foo; +})); +assertType('3|GenericUnions\Foo', getWithDefault(3, new Foo)); +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, new Foo)); +assertType('3|string', getWithDefaultCallable(3, new InvokableClass)); diff --git a/tests/PHPStan/Analyser/data/generics-default.php b/tests/PHPStan/Analyser/nsrt/generics-default.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-default.php rename to tests/PHPStan/Analyser/nsrt/generics-default.php diff --git a/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php new file mode 100644 index 0000000000..d00b8b699a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php @@ -0,0 +1,148 @@ + + */ +function test2($param): Foo +{ + +} + +/** @template T */ +class Foo +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('array<1>', test(1)); + assertType('GenericsDoNotGeneralize\Foo', test2(1)); + assertType('GenericsDoNotGeneralize\Foo', new Foo(1)); +}; + +class Test +{ + public const CONST_A = 1; + public const CONST_B = 2; + + /** + * @return self::CONST_* + */ + public static function foo(): int + { + return self::CONST_A; + } +} + +/** + * Produces a new array of elements by mapping each element in collection through a transformation function (callback). + * Callback arguments will be element, index, collection + * + * @template K of array-key + * @template V + * @template V2 + * + * @param iterable $collection + * @param callable(V,K,iterable):V2 $callback + * + * @return ($collection is list ? list : array) + * + * @no-named-arguments + */ +function map($collection, callable $callback) +{ + $aggregation = []; + + foreach ($collection as $index => $element) { + $aggregation[$index] = $callback($element, $index, $collection); + } + + return $aggregation; +} + +function (): void { + $foo = Test::foo(); + + assertType('1|2', $foo); + + $bar = map([new Test()], static fn(Test $test) => $test::foo()); + + assertType('list<1|2>', $bar); +}; + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', new ArrayIterator($a)); +}; + +/** + * @template K of array-key + * @template V + * @param array $a + * @return ArrayIterator + */ +function createArrayIterator(array $a): ArrayIterator +{ + +} + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', createArrayIterator($a)); +}; + +/** @template T */ +class FooInvariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +/** @template-covariant T */ +class FooCovariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('GenericsDoNotGeneralize\\FooInvariant', new FooInvariant(1)); + assertType('GenericsDoNotGeneralize\\FooCovariant<1>', new FooCovariant(1)); +}; diff --git a/tests/PHPStan/Analyser/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/data/generics-reduce-types-first.php b/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-reduce-types-first.php rename to tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php similarity index 92% rename from tests/PHPStan/Analyser/data/generics.php rename to tests/PHPStan/Analyser/nsrt/generics.php index 2f0afe2259..1968fab471 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -96,8 +96,8 @@ function testD($int, $float, $intFloat) assertType('float|int', d($int, $float)); assertType('DateTime|int', d($int, new \DateTime())); assertType('DateTime|float|int', d($intFloat, new \DateTime())); - assertType('array()|DateTime', d([], new \DateTime())); - assertType('array(\'blabla\' => string)|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); + assertType('array{}|DateTime', d([], new \DateTime())); + assertType('array{blabla: \'barrrr\'}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); } /** @@ -131,7 +131,7 @@ function f($a, $b) { $result = []; assertType('array', $a); - assertType('callable(A (function PHPStan\Generics\FunctionsAssertType\f(), argument)): B (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $b); + assertType('callable(A): B', $b); foreach ($a as $k => $v) { assertType('A (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $v); $newV = $b($v); @@ -147,10 +147,10 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('Closure(int): string&numeric', function (int $a): string { + assertType('Closure(int): (lowercase-string&numeric-string&uppercase-string)', function (int $a): string { return (string)$a; }); - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); assertType('Closure(mixed): string', function ($a): string { @@ -159,12 +159,12 @@ function testF($arrayOfInt, $callableOrNull) assertType('array', f($arrayOfInt, function ($a): string { return (string)$a; })); - assertType('array', f($arrayOfInt, function ($a) { + assertType('array', f($arrayOfInt, function ($a) { return $a; })); assertType('array', f($arrayOfInt, $callableOrNull)); - assertType('array', f($arrayOfInt, null)); - assertType('array', f($arrayOfInt, '')); + assertType('array', f($arrayOfInt, null)); + assertType('array', f($arrayOfInt, '')); } /** @@ -224,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** @@ -347,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; } @@ -371,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); @@ -384,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()); } } @@ -741,7 +741,7 @@ function testClasses() assertType('DateTime', $ab->getB(new \DateTime())); $noConstructor = new NoConstructor(1); - assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); + assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); assertType('stdClass', acceptsClassString(\stdClass::class)); assertType('class-string', returnsClassString(new \stdClass())); @@ -763,7 +763,7 @@ function testClasses() $factory = new Factory(new \DateTime(), new A(1)); assertType( - 'array(DateTime, PHPStan\Generics\FunctionsAssertType\A, string, PHPStan\Generics\FunctionsAssertType\A)', + 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, \'\', PHPStan\\Generics\\FunctionsAssertType\\A}', $factory->create(new \DateTime(), '', new A(new \DateTime())) ); } @@ -930,8 +930,8 @@ public function returnStatic(): self function () { $stdEmpty = new StdClassCollection([]); - assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<(int|string), stdClass>', $stdEmpty); - assertType('array', $stdEmpty->getAll()); + assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<*NEVER*, *NEVER*>', $stdEmpty); + assertType('array{}', $stdEmpty->getAll()); $std = new StdClassCollection([new \stdClass()]); assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection', $std); @@ -952,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); } /** @@ -965,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); } } @@ -1034,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); } /** @@ -1089,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; } @@ -1108,6 +1108,7 @@ function () { class GenericReflectionClass extends \ReflectionClass { + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1121,6 +1122,7 @@ public function newInstanceWithoutConstructor() class SpecificReflectionClass extends \ReflectionClass { + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1174,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 { @@ -1207,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); @@ -1220,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); }; /** @@ -1398,12 +1415,12 @@ public function process($class): void { } function (\Throwable $e): void { - assertType('mixed', $e->getCode()); + assertType('(int|string)', $e->getCode()); }; function (): void { $array = ['a' => 1, 'b' => 2]; - assertType('array(\'a\' => int, \'b\' => int)', a($array)); + assertType('array{a: 1, b: 2}', a($array)); }; @@ -1539,11 +1556,11 @@ function arrayBound5(array $a): array } function (): void { - assertType('array(1 => true)', arrayBound1([1 => true])); - assertType("array('a', 'b', 'c')", arrayBound2(range('a', 'c'))); + 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{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'))); }; @@ -1558,5 +1575,5 @@ function constantArrayBound(array $a): array } function (): void { - assertType("array('string', true)", constantArrayBound(['string', true])); + 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/getenv-php74.php b/tests/PHPStan/Analyser/nsrt/getenv-php74.php new file mode 100644 index 0000000000..a100e47686 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/getenv-php74.php @@ -0,0 +1,23 @@ +', getenv()); + assertType('string|false', getenv('foo')); + + assertType('string|false', getenv($stringOrNull)); + assertType('string|false', getenv($mixed)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/getenv-php80.php b/tests/PHPStan/Analyser/nsrt/getenv-php80.php new file mode 100644 index 0000000000..c5036fa153 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/getenv-php80.php @@ -0,0 +1,20 @@ += 8.0 + +namespace GetenvPHP80; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function test(string|null $stringOrNull, mixed $mixed) + { + assertType('array', getenv(null)); + assertType('array', getenv()); + assertType('string|false', getenv('foo')); + + assertType('array|string|false', getenv($stringOrNull)); + assertType('array|string|false', getenv($mixed)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/getopt.php b/tests/PHPStan/Analyser/nsrt/getopt.php new file mode 100644 index 0000000000..aae0e128e5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/getopt.php @@ -0,0 +1,10 @@ +|string|false>|false)', $opts); +assertType('int<1, max>', $restIndex); diff --git a/tests/PHPStan/Analyser/nsrt/gettype.php b/tests/PHPStan/Analyser/nsrt/gettype.php new file mode 100644 index 0000000000..a5b0d751ec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gettype.php @@ -0,0 +1,54 @@ +', $GLOBALS); +\PHPStan\Testing\assertType('array', $_SERVER); +\PHPStan\Testing\assertType('array', $_GET); +\PHPStan\Testing\assertType('array', $_POST); +\PHPStan\Testing\assertType('array', $_FILES); +\PHPStan\Testing\assertType('array', $_COOKIE); +\PHPStan\Testing\assertType('array', $_SESSION); +\PHPStan\Testing\assertType('array', $_REQUEST); +\PHPStan\Testing\assertType('array', $_ENV); diff --git a/tests/PHPStan/Analyser/data/graphics-draw-return-types.php b/tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/graphics-draw-return-types.php rename to tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php new file mode 100644 index 0000000000..eacfb06af6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -0,0 +1,163 @@ +>', $fileErrorsCounts); + assertType('int<1, max>', $fileErrorsCounts[$errorMessage]); + $fileErrorsCounts[$errorMessage] = 1; + assertType('non-empty-array>', $fileErrorsCounts); + assertType('1', $fileErrorsCounts[$errorMessage]); + continue; + } + + assertType('array>', $fileErrorsCounts); + assertType('int<1, max>', $fileErrorsCounts[$errorMessage]); + + $fileErrorsCounts[$errorMessage]++; + + assertType('non-empty-array>', $fileErrorsCounts); + assertType('int<2, max>', $fileErrorsCounts[$errorMessage]); + } + + assertType('array>', $fileErrorsCounts); + } + + /** + * @param mixed[] $result + * @return void + */ + public function doBar(array $result): void + { + assertType('array', $result); + assert($result['totals']['file_errors'] === 3); + assertType("array", $result); + assertType("mixed", $result['totals']); + assertType('3', $result['totals']['file_errors']); + assertType('mixed', $result['totals']['errors']); + assert($result['totals']['errors'] === 0); + assertType("array", $result); + assertType("mixed", $result['totals']); + assertType('3', $result['totals']['file_errors']); + assertType('0', $result['totals']['errors']); + } + + /** + * @param array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null} $range + * @return void + */ + public function testIsset($range): void + { + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + if (isset($range['min']) || isset($range['max'])) { + assertType("non-empty-array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } else { + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } + + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } + +} + +class TryMixed +{ + + public function doFoo($mixed) + { + if (isset($mixed[0])) { + assertType("mixed~null", $mixed[0]); + assertType("mixed~null", $mixed); + } else { + assertType("mixed", $mixed); + } + + assertType("mixed", $mixed); + } + + public function doFoo2($mixed) + { + if (isset($mixed['foo'])) { + assertType("mixed~null", $mixed['foo']); + assertType("mixed~null", $mixed); + } else { + assertType("mixed", $mixed); + } + + assertType("mixed", $mixed); + } + + public function doBar(\SimpleXMLElement $xml) + { + if (isset($xml['foo'])) { + assertType('SimpleXMLElement', $xml['foo']); + assertType("SimpleXMLElement&hasOffset('foo')", $xml); + } + } + +} + + +class AssignVsNarrow +{ + + /** + * @param array{a: string} $a + * @return void + */ + public function doFoo(array $a) + { + if (is_int($a['a'])) { + assertType('*NEVER*', $a); + } + } + + /** + * @param array{a: string} $a + * @return void + */ + public function doBar(array $a, int $i) + { + $a['a'] = $i; + assertType('array{a: int}', $a); + } + + /** + * @param array $a + * @return void + */ + public function doFoo2(array $a) + { + if (is_int($a['a'])) { + assertType("non-empty-array&hasOffsetValue('a', *NEVER*)", $a); + } + } + + /** + * @param array $a + * @return void + */ + public function doBar2(array $a, int $i, string $s) + { + $a['a'] = $i; + assertType('non-empty-array&hasOffsetValue(\'a\', int)', $a); + $a['a'] = $s; + assertType('non-empty-array&hasOffsetValue(\'a\', string)', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php new file mode 100644 index 0000000000..2ffcb920f8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php @@ -0,0 +1,56 @@ += 8.0 + +namespace HashFunctions; + +use function PHPStan\Testing\assertType; + +class HashFunctionTests80 +{ + + public function hash_hmac(string $string): void + { + assertType('*NEVER*', hash_hmac('crc32', 'data', 'key')); + assertType('*NEVER*', hash_hmac('invalid', 'data', 'key')); + assertType('lowercase-string&non-falsy-string', hash_hmac($string, 'data', 'key')); + assertType('non-falsy-string', hash_hmac($string, 'data', 'key', true)); + } + + public function hash_hmac_file(): void + { + assertType('*NEVER*', hash_hmac_file('crc32', 'filename', 'key')); + assertType('*NEVER*', hash_hmac_file('invalid', 'filename', 'key')); + } + + public function hash(string $string): void + { + assertType('*NEVER*', hash('invalid', 'data', false)); + assertType('lowercase-string&non-falsy-string', hash($string, 'data')); + } + + public function hash_file(): void + { + assertType('*NEVER*', hash_file('invalid', 'filename', false)); + } + + public function hash_hkdf(string $string): void + { + assertType('*NEVER*', hash_hkdf('crc32', 'key')); + assertType('*NEVER*', hash_hkdf('invalid', 'key')); + assertType('non-falsy-string', hash_hkdf($string, 'key')); + } + + public function hash_pbkdf2(string $string): void + { + assertType('*NEVER*', hash_pbkdf2('crc32', 'password', 'salt', 1000)); + assertType('*NEVER*', hash_pbkdf2('invalid', 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($string, 'password', 'salt', 1000)); + } + + public function caseSensitive() + { + assertType('*NEVER*', hash_hkdf('CRC32', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions.php b/tests/PHPStan/Analyser/nsrt/hash-functions.php new file mode 100644 index 0000000000..72f977baae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/hash-functions.php @@ -0,0 +1,84 @@ +', $http_response_header); +assertNativeType('array', $http_response_header); diff --git a/tests/PHPStan/Analyser/nsrt/ibm_db2.php b/tests/PHPStan/Analyser/nsrt/ibm_db2.php new file mode 100644 index 0000000000..ca132f98eb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ibm_db2.php @@ -0,0 +1,20 @@ +', idate('j')); + assertType('int<1, 7>', idate('N')); + assertType('int', idate('Y')); + assertType('false', idate('wrong')); + assertType('false', idate('')); + assertType('int|false', idate($string)); + assertType('int<0, 23>', idate($hour)); + assertType('int|false', idate($format)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/identical.php b/tests/PHPStan/Analyser/nsrt/identical.php new file mode 100644 index 0000000000..abcd1d5364 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/identical.php @@ -0,0 +1,102 @@ + 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + +} + +class Bar +{ + + public function doFoo(\stdClass $a, \stdClass $b): void + { + assertType('true', $a === $a); + assertType('bool', $a === $b); + assertType('false', $a !== $a); + assertType('bool', $a !== $b); + + assertType('bool', self::createStdClass() === self::createStdClass()); + assertType('bool', self::createStdClass() !== self::createStdClass()); + } + + public static function createStdClass(): \stdClass + { + + } + + public function doBar(array $arr1, array $arr2): void + { + /** + * @var array{foo: bool, bar: int} $arr1 + * @var array{foo: bool, bar: int} $arr2 + */ + if ($arr1 === $arr2) { + assertType('array{foo: bool, bar: int}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } else { + assertType('array{foo: bool, bar: int}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } + + /** + * @var array{foo: true, bar: 17} $arr1 + * @var array{foo: bool, bar: int} $arr2 + */ + if ($arr1 === $arr2) { + assertType('array{foo: true, bar: 17}', $arr1); + assertType('array{foo: true, bar: 17}', $arr2); + } else { + assertType('array{foo: true, bar: 17}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/image-size.php b/tests/PHPStan/Analyser/nsrt/image-size.php new file mode 100644 index 0000000000..a68a0706b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/image-size.php @@ -0,0 +1,43 @@ +', $width); + assertType('int<0, max>', $height); + assertType('int', $type); + assertType('string', $attr); + assertType('string', $imagesize['mime']); + assertType('int', $imagesize['channels']); + assertType('int', $imagesize['bits']); +} + +function imagesizeFoo(string $s): void +{ + $imagesize = getimagesizefromstring($s); + if ($imagesize === false) { + return; + } + list($width, $height, $type, $attr) = $imagesize; + + assertType('int<0, max>', $width); + assertType('int<0, max>', $height); + assertType('int', $type); + assertType('string', $attr); + assertType('string', $imagesize['mime']); + assertType('int', $imagesize['channels']); + assertType('int', $imagesize['bits']); +} + + diff --git a/tests/PHPStan/Analyser/nsrt/imagick-pixel.php b/tests/PHPStan/Analyser/nsrt/imagick-pixel.php new file mode 100644 index 0000000000..8904482f3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/imagick-pixel.php @@ -0,0 +1,15 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor()); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor(0)); + assertType('array{r: float, g: float, b: float, a: float}', $imagickPixel->getColor(1)); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>}', $imagickPixel->getColor(2)); + assertType('array{}', $imagickPixel->getColor(3)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php new file mode 100644 index 0000000000..8e97e19f72 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -0,0 +1,71 @@ + $arr + */ + public function ints(array $arr, int $i) + { + assertType("lowercase-string&uppercase-string", implode($arr)); + assertType("lowercase-string&non-empty-string&uppercase-string", implode([$i, $i])); + if ($i !== 0) { + assertType("lowercase-string&non-falsy-string&uppercase-string", implode([$i, $i])); + } + } + + const X = 'x'; + const ONE = 1; + + public function constants() { + assertType("'12345'", implode(['12', '345'])); + + assertType("'12345'", implode('', ['12', '345'])); + assertType("'12345'", join('', ['12', '345'])); + + assertType("'12,345'", implode(',', ['12', '345'])); + assertType("'12,345'", join(',', ['12', '345'])); + + assertType("'x,345'", join(',', [self::X, '345'])); + assertType("'1,345'", join(',', [self::ONE, '345'])); + } + + /** @param array{0: 1|2, 1: 'a'|'b'} $constArr */ + public function constArrays($constArr) { + assertType("'1a'|'1b'|'2a'|'2b'", implode('', $constArr)); + } + + /** @param array{0: 1|2|3, 1: 'a'|'b'|'c'} $constArr */ + public function constArrays2($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2: 'x'|'y'} $constArr */ + public function constArrays3($constArr) { + assertType("'1ax'|'1ay'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2?: 'x'|'y'} $constArr */ + public function constArrays4($constArr) { + assertType("'1a'|'1ax'|'1ay'|'1b'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{10: 1|2|3, xy: 'a'|'b'|'c'} $constArr */ + public function constArrays5($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 3?: 'c'|'d', 4?: 'e'|'f', 5?: 'g'|'h', 6?: 'x'|'y'} $constArr */ + public function constArrays6($constArr) { + assertType("lowercase-string&non-falsy-string", implode('', $constArr)); + } + + /** @param array{10: 1|2|bool, xy: 'a'|'b'|'c'} $constArr */ + public function constArrays7($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'a'|'b'|'c'", implode('', $constArr)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php b/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php new file mode 100644 index 0000000000..314b3e7bfb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php @@ -0,0 +1,15 @@ +', connection_status()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-constructor.php b/tests/PHPStan/Analyser/nsrt/impure-constructor.php new file mode 100644 index 0000000000..d992b822ba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-constructor.php @@ -0,0 +1,130 @@ +active = false; + } + + public function getActive(): bool + { + return $this->active; + } + + public function activate(): void + { + $this->active = true; + } + +} + + +class ClassWithImpureConstructorNotMarked +{ + + public function __construct(Foo $foo) + { + $foo->activate(); + } + +} + +class ClassWithImpureConstructorMarked +{ + + /** + * @phpstan-impure + */ + public function __construct(Foo $foo) + { + $foo->activate(); + } + +} + +class ClassWithPureConstructorMarked +{ + + /** + * @phpstan-pure + */ + public function __construct(Foo $foo) + { + } + +} + +class ClassWithImpureConstructorNotMarkedWithoutParameters +{ + + /** @var string */ + private $lorem; + + public function __construct() + { + $this->lorem = 'lorem'; + } + +} + +class Test +{ + + public function testClassWithImpureConstructorNotMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorNotMarked($foo); + assertType('false', $foo->getActive()); + } + + public function testClassWithImpureConstructorMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorMarked($foo); + assertType('bool', $foo->getActive()); + } + + public function testClassWithPureConstructorMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithPureConstructorMarked($foo); + assertType('false', $foo->getActive()); + } + + public function testClassWithImpureConstructorNotMarkedWithoutParameters() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorNotMarkedWithoutParameters(); + assertType('false', $foo->getActive()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-error-log.php b/tests/PHPStan/Analyser/nsrt/impure-error-log.php new file mode 100644 index 0000000000..082112b83b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-error-log.php @@ -0,0 +1,14 @@ +fooProp = rand(0, 1); + } + + /** + * @return $this + */ + public function returnsThis($arg) + { + $this->fooProp = rand(0, 1); + } + + /** + * @return $this + * @phpstan-impure + */ + public function returnsThisImpure($arg) + { + $this->fooProp = rand(0, 1); + } + + public function ordinaryMethod(): int + { + return 1; + } + + /** + * @phpstan-impure + * @return int + */ + public function impureMethod(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + /** + * @impure + * @return int + */ + public function impureMethod2(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + public function doFoo(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->voidMethod(); + assertType('int', $this->fooProp); + } + + public function doFluent(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->returnsThis(new stdClass()); + assertType('int', $this->fooProp); + } + + public function doFluent2(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->phpDocReturnThis(); + assertType('int', $this->fooProp); + } + + public function doBar(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->ordinaryMethod(); + assertType('1', $this->fooProp); + } + + public function doBaz(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod(); + assertType('int', $this->fooProp); + } + + public function doLorem(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod2(); + assertType('int', $this->fooProp); + } + +} + +class Person +{ + + public function getName(): ?string + { + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThis($p); + assertType('string', $p->getName()); + } + + public function doFoo2(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThisImpure($p); + assertType('string|null', $p->getName()); + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/in-array-enum.php b/tests/PHPStan/Analyser/nsrt/in-array-enum.php new file mode 100644 index 0000000000..66ae579980 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-enum.php @@ -0,0 +1,77 @@ += 8.1 + +declare(strict_types=1); + +namespace InArrayEnum; + +use function PHPStan\Testing\assertType; + +enum FooUnitEnum +{ + case A; + case B; +} + +class Foo +{ + + /** + * @param array $strings + * @param array $ints + */ + public function nonConstantValues(FooUnitEnum $a, array $strings, array $ints): void + { + assertType('false', in_array($a, $strings, true)); + assertType('false', in_array($a, $strings, false)); + assertType('false', in_array($a, $strings)); + + assertType('bool', in_array($a->name, $strings, true)); + assertType('bool', in_array($a->name, $strings, false)); + assertType('bool', in_array($a->name, $strings)); + + assertType('false', in_array($a->name, $ints, true)); + assertType('bool', in_array($a->name, $ints, false)); + assertType('bool', in_array($a->name, $ints)); + } + + public function looseCheckEnumSpecifyNeedle(mixed $v): void + { + if (in_array($v, FooUnitEnum::cases())) { + assertType('InArrayEnum\FooUnitEnum::A|InArrayEnum\FooUnitEnum::B', $v); + + if (in_array($v, ['A', null, FooUnitEnum::B])) { + assertType('InArrayEnum\FooUnitEnum::B', $v); + } + } + + } + + /** @param array $haystack */ + public function looseCheckEnumSpecifyHaystack(array $haystack): void + { + if (! in_array(FooUnitEnum::A, $haystack)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? FooUnitEnum::A : FooUnitEnum::B, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : rand(), $haystack, true)) { + assertType('array', $haystack); + } + } + + /** @param array $haystack */ + public function skipUnsafeLooseComparison(?FooUnitEnum $v, array $haystack): void + { + if (in_array($v, $haystack, false)) { + assertType('InArrayEnum\FooUnitEnum|null', $v); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php b/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php new file mode 100644 index 0000000000..ee1757521a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php @@ -0,0 +1,18 @@ + $haystack */ + public function specifyHaystack(array $haystack): void + { + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php b/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php new file mode 100644 index 0000000000..999dbc8ff3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php @@ -0,0 +1,28 @@ + $array + */ + public function sayHello(array $array): void + { + if(in_array("thing", $array, true)){ + assertType('non-empty-list', $array); + } + } + + /** @param array $haystack */ + public function nonConstantNeedle(int $needle, array $haystack): void + { + if (in_array($needle, $haystack, true)) { + assertType('non-empty-array', $haystack); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array.php b/tests/PHPStan/Analyser/nsrt/in-array.php new file mode 100644 index 0000000000..7b0cf403da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array.php @@ -0,0 +1,61 @@ + $strings */ + public function doBar(int $i, array $strings): void + { + assertType('bool', in_array($i, $strings)); + assertType('bool', in_array($i, $strings, false)); + assertType('false', in_array($i, $strings, true)); + assertType('false', in_array(1, $strings, true)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in_array_loose.php b/tests/PHPStan/Analyser/nsrt/in_array_loose.php new file mode 100644 index 0000000000..78d2899b8c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in_array_loose.php @@ -0,0 +1,48 @@ += 8.0 + +namespace InArrayLoose; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function looseComparison( + string $string, + int $int, + float $float, + bool $bool, + string|int $stringOrInt, + string|null $stringOrNull, + ): void { + if (in_array($string, ['1', 'a'])) { + assertType("'1'|'a'", $string); + } + if (in_array($string, [1, 'a'])) { + assertType("string", $string); // could be '1'|'a' + } + if (in_array($int, [1, 2])) { + assertType('1|2', $int); + } + if (in_array($int, ['1', 2])) { + assertType('int', $int); // could be 1|2 + } + if (in_array($bool, [true])) { + assertType('true', $bool); + } + if (in_array($bool, [true, null])) { + assertType('bool', $bool); + } + if (in_array($float, [1.0, 2.0])) { + assertType('1.0|2.0', $float); + } + if (in_array($float, ['1', 2.0])) { + assertType('float', $float); // could be 1.0|2.0 + } + if (in_array($stringOrInt, ['1', '2'])) { + assertType('int|string', $stringOrInt); // could be '1'|'2'|1|2 + } + if (in_array($stringOrNull, ['1', 'a'])) { + assertType('string|null', $stringOrNull); // could be '1'|'a' + } + } +} diff --git a/tests/PHPStan/Analyser/data/inc-dec-in-conditions.php b/tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php similarity index 100% rename from tests/PHPStan/Analyser/data/inc-dec-in-conditions.php rename to tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php diff --git a/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php b/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php new file mode 100644 index 0000000000..8bddb79eca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php @@ -0,0 +1,41 @@ +doFoo()); + assertType('mixed', $foo->doBar()); +}; diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php similarity index 89% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php index e340baba85..087d2893af 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php @@ -32,7 +32,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } @@ -75,7 +75,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array', $this->doFoo(1, 'hahaha')); } } @@ -92,7 +92,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php diff --git a/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-return-type-with-narrower-native-return-type-php8.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-return-type-with-narrower-native-return-type-php8.php new file mode 100644 index 0000000000..6e1ff61c3a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-return-type-with-narrower-native-return-type-php8.php @@ -0,0 +1,32 @@ += 8.0 + +namespace InheritPhpDocReturnTypeWithNarrowerNativeReturnTypePhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @return array|positive-int|null + */ + public function doFoo(): array|int|null + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): array|int + { + + } + +} + +function (Bar $bar): void { + assertType('array|int<1, max>', $bar->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/data/inheritdoc-constructors.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php similarity index 100% rename from tests/PHPStan/Analyser/data/inheritdoc-constructors.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php diff --git a/tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php similarity index 100% rename from tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php diff --git a/tests/PHPStan/Analyser/nsrt/ini-get.php b/tests/PHPStan/Analyser/nsrt/ini-get.php new file mode 100644 index 0000000000..6751b3cc16 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ini-get.php @@ -0,0 +1,29 @@ + $one + * @param int-mask $two + * @param int-mask<1, 2, 8> $three + * @param int-mask<1, 4, 16, 64, 256, 1024> $four + * @param int-mask-of $five + */ + public static function test(int $one, int $two, int $three, int $four, int $five): void + { + assertType('int<0, 3>', $one); + assertType('int<0, 3>', $two); + assertType('0|1|2|3|8|9|10|11', $three); + assertType('0|1|4|5|16|17|20|21|64|65|68|69|80|81|84|85|256|257|260|261|272|273|276|277|320|321|324|325|336|337|340|341|1024|1025|1028|1029|1040|1041|1044|1045|1088|1089|1092|1093|1104|1105|1108|1109|1280|1281|1284|1285|1296|1297|1300|1301|1344|1345|1348|1349|1360|1361|1364|1365', $four); + assertType('0|1|4|5', $five); + } + + /** + * @param int-mask-of $one + * @param int-mask<0, 1, false> $two + */ + public static function invalid(int $one, int $two, int $three): void + { + assertType('int', $one); // not all constant integers + assertType('int', $two); // not all constant integers + } +} diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php new file mode 100644 index 0000000000..8adf56f1c1 --- /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('mixed', $this->error1); + assertType('mixed', $this->error2); + assertType('int', $this->int); + } + + /** + * @param int $i + * @param 1|2|3 $j + * @param 1|-20|3 $z + * @param positive-int $pi + * @param int<1, 10> $r1 + * @param int<5, 10> $r2 + * @param int<-9, 100> $r3 + * @param int $rMin + * @param int<5, max> $rMax + * + * @param 20|40|60 $x + * @param 2|4 $y + */ + public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) { + assertType('int', $r1 + $i); + assertType('int', $r1 - $i); + assertType('int', $r1 * $i); + assertType('(float|int)', $r1 / $i); + + assertType('int<2, 13>', $r1 + $j); + assertType('int<-2, 9>', $r1 - $j); + assertType('int<1, 30>', $r1 * $j); + assertType('float|int<1, 10>', $r1 / $j); + assertType('int', $rMin * $j); + assertType('int<5, max>', $rMax * $j); + + assertType('int<2, 13>', $j + $r1); + assertType('int<-9, 2>', $j - $r1); + assertType('int<1, 30>', $j * $r1); + assertType('float|int<1, 3>', $j / $r1); + assertType('int', $j * $rMin); + assertType('int<5, max>', $j * $rMax); + + assertType('int<-19, -10>|int<2, 13>', $r1 + $z); + assertType('int<-2, 9>|int<21, 30>', $r1 - $z); + assertType('int<-200, -20>|int<1, 30>', $r1 * $z); + assertType('float|int<1, 10>', $r1 / $z); + assertType('int', $rMin * $z); + assertType('int|int<5, max>', $rMax * $z); + + assertType('int<2, max>', $pi + 1); + assertType('int<-1, max>', $pi - 2); + assertType('int<2, max>', $pi * 2); + assertType('float|int<1, max>', $pi / 2); + assertType('int<2, max>', 1 + $pi); + assertType('int', 2 - $pi); + assertType('int<2, max>', 2 * $pi); + assertType('float|int<1, 2>', 2 / $pi); + + assertType('int<5, 14>', $r1 + 4); + assertType('int<-3, 6>', $r1 - 4); + assertType('int<4, 40>', $r1 * 4); + assertType('float|int<1, 2>', $r1 / 4); + assertType('int<9, max>', $rMax + 4); + assertType('int<1, max>', $rMax - 4); + assertType('int<20, max>', $rMax * 4); + assertType('float|int<2, max>', $rMax / 4); + + assertType('int<6, 20>', $r1 + $r2); + assertType('int<-9, 5>', $r1 - $r2); + assertType('int<5, 100>', $r1 * $r2); + assertType('float|int<1, 2>', $r1 / $r2); + + assertType('int<-99, 19>', $r1 - $r3); + + assertType('int', $r1 + $rMin); + assertType('int<-4, max>', $r1 - $rMin); + assertType('int', $r1 * $rMin); + assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin); + assertType('int', $rMin + $r1); + assertType('int', $rMin - $r1); + assertType('int', $rMin * $r1); + assertType('float|int', $rMin / $r1); + + assertType('int<6, max>', $r1 + $rMax); + assertType('int', $r1 - $rMax); + assertType('int<5, max>', $r1 * $rMax); + assertType('float|int<1, 2>', $r1 / $rMax); + assertType('int<6, max>', $rMax + $r1); + assertType('int<-5, max>', $rMax - $r1); + assertType('int<5, max>', $rMax * $r1); + assertType('float|int<1, max>', $rMax / $r1); + + assertType('5|10|15|20|30', $x / $y); + + assertType('float|int<1, max>', $rMax / $rMax); + assertType('(float|int)', $rMin / $rMin); + } + + /** + * @param int<0, max> $a + * @param int<0, max> $b + * @param int<16, 32> $c + * @param int<2, 4> $d + */ + function divisionLoosesInformation(int $a, int $b, int $c, int $d): void { + assertType('float|int<0, max>', $a / $b); + assertType('float|int<8, 16>', $c / 2); + assertType('float|int<4, 16>', $c / $d); + } + + /** + * @param int $rMin + * @param int<5, max> $rMax + * + * @see https://www.wolframalpha.com/input/?i=%28interval%5B2%2C%E2%88%9E%5D+%2F+-1%29 + * @see https://3v4l.org/ur9Wf + */ + public function maximaInversion($rMin, $rMax) { + assertType('int<-5, max>', -1 * $rMin); + assertType('int', -2 * $rMax); + + assertType('int<-5, max>', $rMin * -1); + assertType('int', $rMax * -2); + + assertType('-1|1|float', -1 / $rMin); + assertType('float', -2 / $rMax); + + assertType('float|int<-5, max>', $rMin / -1); + assertType('float|int', $rMax / -2); + } + + /** + * @param int<1, 10> $r1 + * @param int<-5, 10> $r2 + * @param int $rMin + * @param int<5, max> $rMax + * @param int<0, 50> $rZero + */ + public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { + + assertType('int<-10, -1>', -$r1); + assertType('int<-10, 5>', -$r2); + assertType('int<-5, max>', -$rMin); + assertType('int', -$rMax); + assertType('int<-50, 0>', -$rZero); + } + + /** + * @param int<-1, 2> $p + * @param int<-1, 2> $u + */ + public function sayHello($p, $u): void + { + assertType('int<-2, 4>', $p + $u); + assertType('int<-3, 3>', $p - $u); + assertType('int<-2, 4>', $p * $u); + assertType('float|int<-2, 2>', $p / $u); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftLeft($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a << 0); + assertType('int<5, max>', $b << 0); + assertType('int', $c << 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d << 0); + assertType('1|3|5', $e << 0); + assertType('*ERROR*', $f << 0); + + assertType('int<-10, 10>', $a << 1); + assertType('int<10, max>', $b << 1); + assertType('int', $c << 1); + assertType('2|50|int<10, 20>|int<60, 80>', $d << 1); + assertType('2|6|10', $e << 1); + assertType('*ERROR*', $f << 1); + + assertType('*ERROR*', $a << -1); + + assertType('int', $a << $b); + + assertType('0', null << 1); + assertType('0', false << 1); + assertType('2', true << 1); + assertType('10', "10" << 0); + assertType('*ERROR*', "ciao" << 0); + assertType('30', 15.9 << 1); + assertType('*ERROR*', array(5) << 1); + + assertType('8', 4.1 << 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float << 1.9); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftRight($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a >> 0); + assertType('int<5, max>', $b >> 0); + assertType('int', $c >> 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d >> 0); + assertType('1|3|5', $e >> 0); + assertType('*ERROR*', $f >> 0); + + assertType('int<-3, 2>', $a >> 1); + assertType('int<2, max>', $b >> 1); + assertType('int', $c >> 1); + assertType('0|12|int<2, 5>|int<15, 20>', $d >> 1); + assertType('0|1|2', $e >> 1); + assertType('*ERROR*', $f >> 1); + + assertType('*ERROR*', $a >> -1); + + assertType('int', $a >> $b); + + assertType('0', null >> 1); + assertType('0', false >> 1); + assertType('0', true >> 1); + assertType('10', "10" >> 0); + assertType('*ERROR*', "ciao" >> 0); + assertType('7', 15.9 >> 1); + assertType('*ERROR*', array(5) >> 1); + + assertType('2', 4.1 >> 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float >> 1.9); + } + + /** + * @param int<0, max> $positive + * @param int $negative + */ + public function zeroIssues($positive, $negative) + { + assertType('0', 0 * $positive); + assertType('int<0, max>', $positive * $positive); + assertType('0', 0 * $negative); + assertType('int<0, max>', $negative * $negative); + assertType('int', $negative * $positive); + } + +} + +function subtract($m) { + if ($m != 0) { + assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" + assertType('int', (int) $m); + } + if ($m !== 0) { + assertType("mixed~0", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_int($m)) { + assertType("mixed~int", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + + if ($m != true) { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType('0', (int) $m); + } + if ($m !== true) { + assertType("mixed~true", $m); + assertType('int', (int) $m); + } + + if ($m != false) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('int', (int) $m); + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_string($m) && !is_float($m)) { + assertType("mixed~(float|string)", $m); + assertType('int', (int) $m); + if ($m != false) { + assertType("mixed~(0|array{}|float|string|false|null)", $m); + assertType('int|int<1, max>', (int) $m); + } + } +} diff --git a/tests/PHPStan/Analyser/data/intersection-static.php b/tests/PHPStan/Analyser/nsrt/intersection-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/intersection-static.php rename to tests/PHPStan/Analyser/nsrt/intersection-static.php diff --git a/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php b/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php new file mode 100644 index 0000000000..5b91643af1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php @@ -0,0 +1,22 @@ +returnsAlias()); + } + + /** @psalm-return MyObject */ + public function returnsAlias() + { + + } +} diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-function.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-function.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-static.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-static.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php diff --git a/tests/PHPStan/Analyser/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/data/is-numeric.php b/tests/PHPStan/Analyser/nsrt/is-numeric.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-numeric.php rename to tests/PHPStan/Analyser/nsrt/is-numeric.php diff --git a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php new file mode 100644 index 0000000000..1469236ea5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php @@ -0,0 +1,55 @@ + $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_subclass_of($a, Bar::class)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_subclass_of($b, Foo::class)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/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/data/iterator-iterator.php b/tests/PHPStan/Analyser/nsrt/iterator-iterator.php similarity index 100% rename from tests/PHPStan/Analyser/data/iterator-iterator.php rename to tests/PHPStan/Analyser/nsrt/iterator-iterator.php diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php new file mode 100644 index 0000000000..038b36f7fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -0,0 +1,67 @@ + $foo + */ + public function testDefaultBehavior(Traversable $foo) + { + assertType('array', iterator_to_array($foo)); + } + + /** + * @param Traversable $foo + */ + public function testExplicitlyPreserveKeys(Traversable $foo) + { + assertType('array', iterator_to_array($foo, true)); + } + + /** + * @param Traversable $foo + */ + public function testNotPreservingKeys(Traversable $foo) + { + assertType('list', iterator_to_array($foo, false)); + } + + public function testBehaviorOnGenerators(): void + { + $generator1 = static function (): iterable { + yield 0 => 1; + yield true => 2; + yield 2 => 3; + yield null => 4; + }; + $generator2 = static function (): iterable { + yield 0 => 1; + yield 'a' => 2; + yield null => 3; + yield true => 4; + }; + + assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('list<1|2|3|4>', iterator_to_array($generator1(), false)); + assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + assertType('list<1|2|3|4>', iterator_to_array($generator2(), false)); + } + + public function testOnGeneratorsWithIllegalKeysForArray(): void + { + $illegalGenerator = static function (): iterable { + yield 'a' => 'b'; + yield new stdClass => 'c'; + }; + + assertType('*NEVER*', iterator_to_array($illegalGenerator())); + assertType('list<\'b\'|\'c\'>', iterator_to_array($illegalGenerator(), false)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php b/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php new file mode 100644 index 0000000000..4919a83bf9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php @@ -0,0 +1,17 @@ += 8.3 + +namespace JsonValidate; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(string $s): void + { + if (json_validate($s)) { + assertType('non-empty-string', $s); + } else { + assertType('string', $s); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/key-exists.php b/tests/PHPStan/Analyser/nsrt/key-exists.php new file mode 100644 index 0000000000..67c42f6c14 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/key-exists.php @@ -0,0 +1,112 @@ += 8.0 + +namespace KeyExists; + +use function key_exists; +use function PHPStan\Testing\assertType; + +class KeyExists +{ + /** + * @param array $a + * @return void + */ + public function doFoo(array $a, string $key, int $anotherKey): void + { + assertType('false', key_exists(2, $a)); + assertType('bool', key_exists('foo', $a)); + assertType('false', key_exists('2', $a)); + + $a = ['foo' => 2, 3 => 'bar']; + assertType('true', key_exists('foo', $a)); + assertType('true', key_exists(3, $a)); + assertType('true', key_exists('3', $a)); + assertType('false', key_exists(4, $a)); + + if (key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + + $empty = []; + assertType('false', key_exists('foo', $empty)); + assertType('false', key_exists($key, $empty)); + } + + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (key_exists($key1, $a)) { + assertType('int', $key1); + } + if (key_exists($key2, $a)) { + assertType('lowercase-string&numeric-string&uppercase-string', $key2); + } + if (key_exists($key3, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); + } + if (key_exists($key4, $a)) { + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); + } + if (key_exists($key5, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); + } + + if (key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (key_exists($key2, $b)) { + assertType('string', $key2); + } + if (key_exists($key3, $b)) { + assertType('string', $key3); + } + if (key_exists($key4, $b)) { + assertType('string', $key4); + } + if (key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (key_exists($key1, $c)) { + assertType('int', $key1); + } + if (key_exists($key2, $c)) { + assertType('string', $key2); + } + if (key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/key-of-generic.php b/tests/PHPStan/Analyser/nsrt/key-of-generic.php new file mode 100644 index 0000000000..71b05a7d3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/key-of-generic.php @@ -0,0 +1,40 @@ + + */ +interface Result +{ + /** + * @return key-of|null + */ + public function getKey(); +} + +/** + * @param Result $result + * @param Result $listResult + * @param Result> $mixedResult + * @param Result> $stringKeyResult + * @param Result> $intKeyResult + * @param Result $emptyResult + */ +function test( + Result $result, + Result $listResult, + Result $mixedResult, + Result $stringKeyResult, + Result $intKeyResult, + Result $emptyResult, +): void { + assertType("'j'|'k'|null", $result->getKey()); + assertType('0|1|null', $listResult->getKey()); + assertType('int|string|null', $mixedResult->getKey()); + assertType('string|null', $stringKeyResult->getKey()); + assertType('int|null', $intKeyResult->getKey()); + assertType('null', $emptyResult->getKey()); +} diff --git a/tests/PHPStan/Analyser/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..f5b37b4410 --- /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, 3?: float|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($lowercase)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_SCHEME)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_HOST)); + assertType('int<0, 65535>|false|null', parse_url($lowercase, PHP_URL_PORT)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_USER)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_PASS)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_PATH)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_QUERY)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_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-convert-encoding-php7.php b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php7.php new file mode 100644 index 0000000000..2fb84826f9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php7.php @@ -0,0 +1,35 @@ + $stringList + * @param list $intList + * @param 'foo'|'bar'|array{foo: string, bar: int, baz: 'foo'}|bool $union + */ +function test_mb_convert_encoding( + mixed $mixed, + string $constantString, + string $string, + array $mixedArray, + array $structuredArray, + array $stringList, + array $intList, + string|array|bool $union, + int $int, +): void { + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed, 'UTF-8')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($constantString, 'UTF-8')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8')); + \PHPStan\Testing\assertType('array|false', mb_convert_encoding($mixedArray, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|false', mb_convert_encoding($structuredArray, 'UTF-8')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($intList, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|string|false', mb_convert_encoding($union, 'UTF-8')); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($int, 'UTF-8')); + + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', 'auto')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', ' AUTO ')); +}; diff --git a/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php new file mode 100644 index 0000000000..e96f8f0e1a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php @@ -0,0 +1,48 @@ += 8.0 + +namespace MbConvertEncodingPHP8; + +/** + * @param 'foo'|'bar' $constantString + * @param array{foo: string, bar: int, baz: 'foo'} $structuredArray + * @param list $stringList + * @param list $intList + * @param 'foo'|'bar'|array{foo: string, bar: int, baz: 'foo'}|bool $union + */ +function test_mb_convert_encoding( + mixed $mixed, + string $constantString, + string $string, + array $mixedArray, + array $structuredArray, + array $stringList, + array $intList, + string|array|bool $union, + int $int, +): void { + \PHPStan\Testing\assertType('array|string', mb_convert_encoding($mixed, 'UTF-8')); + \PHPStan\Testing\assertType('string', mb_convert_encoding($constantString, 'UTF-8')); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8')); + \PHPStan\Testing\assertType('array', mb_convert_encoding($mixedArray, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}', mb_convert_encoding($structuredArray, 'UTF-8')); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8')); + \PHPStan\Testing\assertType('list', mb_convert_encoding($intList, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|string', mb_convert_encoding($union, 'UTF-8')); + \PHPStan\Testing\assertType('array|string', mb_convert_encoding($int, 'UTF-8')); + + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', 'FOO')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', $string)); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', 'FOO,BAR')); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', ['FOO'])); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', ['FOO', 'BAR'])); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', ['FOO,BAR'])); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', 'FOO')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', $string)); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', 'FOO,BAR')); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', ['FOO'])); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', ['FOO', 'BAR'])); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', ['FOO,BAR'])); + + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', 'auto')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', ' AUTO ')); +}; diff --git a/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php new file mode 100644 index 0000000000..5bd069c873 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php @@ -0,0 +1,57 @@ += 8.3 + +declare(strict_types=1); + +namespace MbStrlenPhp83; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp83 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|16|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character-php8.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php index b53353bd3a..2933d4ccab 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php @@ -1,4 +1,4 @@ -= 8.0 \PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<0, 55295>|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character.php index 9dab962ec5..41a921c9f7 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php @@ -1,4 +1,4 @@ -|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('true', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/nsrt/memcache-get.php b/tests/PHPStan/Analyser/nsrt/memcache-get.php new file mode 100644 index 0000000000..533e483682 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/memcache-get.php @@ -0,0 +1,14 @@ +get("key1")); + assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); +}; diff --git a/tests/PHPStan/Analyser/nsrt/minmax-arrays.php b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php new file mode 100644 index 0000000000..bd94a81fb3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php @@ -0,0 +1,176 @@ + 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); + } + if (count($ints) <= 0) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface|false', min(array_filter([$dateB]))); + assertType('DateTimeInterface|false', max(array_filter([$dateB]))); +} + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9|false', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9|false', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} + +/** + * @param int[] $ints + */ +function countMode(array $ints, int $mode): void +{ + if (count($ints, $mode) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) <= 0) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/minmax-php8.php b/tests/PHPStan/Analyser/nsrt/minmax-php8.php new file mode 100644 index 0000000000..2c75f363c9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax-php8.php @@ -0,0 +1,177 @@ += 8.0 + +namespace MinMaxArraysPhp8; + +use function PHPStan\Testing\assertType; + +function dummy(): void +{ + assertType('1', min([1])); + assertType('*ERROR*', min([])); + assertType('1', max([1])); + assertType('*ERROR*', max([])); +} + +/** + * @param int[] $ints + */ +function dummy2(array $ints): void +{ + if (count($ints) === 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) === 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) !== 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) !== 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int', min($ints)); + assertType('int', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface', min(array_filter([$dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateB]))); +} + + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} + +/** + * @param int[] $ints + */ +function countMode(array $ints, int $mode): void +{ + if (count($ints, $mode) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/minmax.php b/tests/PHPStan/Analyser/nsrt/minmax.php new file mode 100644 index 0000000000..d4cbb77c44 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax.php @@ -0,0 +1,66 @@ +|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); + assertType('array{1: true}', array_filter([false, true])); +} + +function dummy6(string $s, string $t): void { + assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); +} + +class HelloWorld +{ + public function setRange(int $range): void + { + if ($range < 0) { + return; + } + assertType('int<0, 100>', min($range, 100)); + assertType('int<0, 100>', min(100, $range)); + } + + public function setRange2(int $range): void + { + if ($range > 100) { + return; + } + assertType('int<0, 100>', max($range, 0)); + assertType('int<0, 100>', max(0, $range)); + } + + public function boundRange(): void + { + /** + * @var int<1, 6> $range + */ + $range = getFoo(); + + assertType('int<1, 4>', min($range, 4)); + assertType('int<4, 6>', max(4, $range)); + } + + public function unionType(): void + { + /** + * @var array{0, 1, 2}|array{4, 5, 6} $numbers2 + */ + $numbers2 = getFoo(); + + assertType('0|4', min($numbers2)); + assertType('2|6', max($numbers2)); + } +} diff --git a/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php similarity index 92% rename from tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php rename to tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php index 43b55bae18..d516f89f23 100644 --- a/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php +++ b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php @@ -43,7 +43,7 @@ public function doFoo() } })()); - \PHPStan\Testing\assertType('array(\'foo\' => \'bar\')', (function () { + \PHPStan\Testing\assertType('array{foo: \'bar\'}', (function () { $array = [ 'foo' => 'bar', ]; diff --git a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php new file mode 100644 index 0000000000..c544802655 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php @@ -0,0 +1,61 @@ += 8.0 + +namespace SubtractMixed; + +use function PHPStan\Testing\assertType; + +/** + * @param int|0.0|''|'0'|array{}|false|null $moreThenFalsy + */ +function subtract(mixed $m, $moreThenFalsy) { + if ($m !== true) { + assertType("mixed~true", $m); + assertType('bool', (bool) $m); // mixed could still contain something truthy + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('bool', (bool) $m); // mixed could still contain something falsy + } + if (!is_bool($m)) { + assertType('mixed~bool', $m); + assertType('bool', (bool) $m); + } + if (!is_array($m)) { + assertType('mixed~array', $m); + assertType('bool', (bool) $m); + } + + if ($m) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('true', (bool) $m); + } + if (!$m) { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType('false', (bool) $m); + } + if (!$m) { + if (!is_int($m)) { + assertType("0.0|''|'0'|array{}|false|null", $m); + assertType('false', (bool)$m); + } + if (!is_bool($m)) { + assertType("0|0.0|''|'0'|array{}|null", $m); + assertType('false', (bool)$m); + } + } + + if (!$m || is_int($m)) { + assertType("0.0|''|'0'|array{}|int|false|null", $m); + assertType('bool', (bool) $m); + } + + if ($m !== $moreThenFalsy) { + assertType('mixed', $m); + assertType('bool', (bool) $m); // could be true + } + + if ($m != 0 && !is_array($m) && $m != null && !is_object($m)) { // subtract more types then falsy + assertType("mixed~(0|0.0|''|'0'|array|object|false|null)", $m); + assertType('true', (bool) $m); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mixed-to-number.php b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php new file mode 100644 index 0000000000..fee2d7bed4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php @@ -0,0 +1,47 @@ +', $mixed); + assertType('(float|int)', $mixed + $mixed); + } +} + +function addingAlphabet() { + $a = 'a'; + $a++; + assertType("'b'", $a); +} diff --git a/tests/PHPStan/Analyser/data/mixed-typehint.php b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php similarity index 95% rename from tests/PHPStan/Analyser/data/mixed-typehint.php rename to tests/PHPStan/Analyser/nsrt/mixed-typehint.php index 8d7ce4ad16..5b3c17cbb1 100644 --- a/tests/PHPStan/Analyser/data/mixed-typehint.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php @@ -1,4 +1,4 @@ -= 8.0 namespace MixedTypehint; diff --git a/tests/PHPStan/Analyser/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/data/more-type-strings.php b/tests/PHPStan/Analyser/nsrt/more-type-strings.php similarity index 100% rename from tests/PHPStan/Analyser/data/more-type-strings.php rename to tests/PHPStan/Analyser/nsrt/more-type-strings.php diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php new file mode 100644 index 0000000000..c8ae927b2d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -0,0 +1,56 @@ +', $enumString); + assertType('literal-string&non-empty-string', $nonEmptyLiteralString); + assertType('float|int|int<1, max>|non-falsy-string|true', $nonEmptyScalar); + assertType("0|0.0|''|'0'|false", $emptyScalar); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + assertType('lowercase-string', $lowercaseString); + assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); + assertType('uppercase-string', $uppercaseString); + assertType('non-empty-string&uppercase-string', $nonEmptyUppercaseString); + } + +} diff --git a/tests/PHPStan/Analyser/data/multi-assign.php b/tests/PHPStan/Analyser/nsrt/multi-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/multi-assign.php rename to tests/PHPStan/Analyser/nsrt/multi-assign.php diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php new file mode 100644 index 0000000000..6f227da9d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php @@ -0,0 +1,15 @@ +query('UPDATE x SET y = 0;'); + assertType('int<-1, max>|numeric-string', $mysqli->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php new file mode 100644 index 0000000000..14aa92bd6b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php @@ -0,0 +1,15 @@ +query('SELECT x FROM z;'); + assertType('int<0, max>|numeric-string', $mysqliResult->num_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php new file mode 100644 index 0000000000..1ab625db4f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php @@ -0,0 +1,20 @@ +prepare('SELECT x FROM z;'); + $stmt->execute(); + assertType('int<0, max>|numeric-string', $stmt->num_rows); + + $stmt = $mysqli->prepare('DELETE FROM z;'); + $stmt->execute(); + assertType('int<-1, max>|numeric-string', $stmt->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php new file mode 100644 index 0000000000..ceb0c6c78d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php @@ -0,0 +1,16 @@ +fetch_object()); + assertType('MysqliFetchObject\MyClass|false|null', $result->fetch_object(MyClass::class)); + + assertType('stdClass|false|null', mysqli_fetch_object($result)); + assertType('MysqliFetchObject\MyClass|false|null', mysqli_fetch_object($result, MyClass::class)); +} + +class MyClass {} + diff --git a/tests/PHPStan/Analyser/nsrt/named-arguments.php b/tests/PHPStan/Analyser/nsrt/named-arguments.php new file mode 100644 index 0000000000..6c30e37f32 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/named-arguments.php @@ -0,0 +1,14 @@ += 8.0 + +namespace NamedArguments; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function array_search() { + $haystack = ['a', 'b', 'c']; + $needle = 'c'; + assertType('2', array_search(strict: true, needle: $needle, haystack: $haystack)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php new file mode 100644 index 0000000000..5687c0b23a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -0,0 +1,98 @@ + $arr */ +function doFoo(string $x, array $arr): void { + if ((bool) strlen($x)) { + assertType('string', $x); // could be non-empty-string + } else { + assertType('string', $x); + } + assertType('string', $x); + + if ((bool) array_search($x, $arr, true)) { + assertType('non-empty-array', $arr); + } else { + assertType('array', $arr); + } + assertType('string', $x); + + if ((bool) preg_match('~.*~', $x, $matches)) { + assertType('array{string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string}', $matches); +} + +/** @param int<-5, 5> $x */ +function castString($x, string $s, bool $b) { + if ((string) $x) { + assertType('int<-5, 5>', $x); + } else { + assertType('0', $x); + } + + if ((string) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((string) strrchr($s, 'xy')) { + assertType('string', $s); // could be non-empty-string + } else { + assertType('string', $s); + } +} + +/** @param int<-5, 5> $x */ +function castInt($x, string $s, bool $b) { + if ((int) $x) { + assertType('int<-5, -1>|int<1, 5>', $x); + } else { + assertType('0', $x); + } + + if ((int) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((int) $s) { + assertType('string', $s); + } else { + assertType('string', $s); + } + + if ((int) strpos($s, 'xy')) { + assertType('string', $s); + } else { + assertType('string', $s); + } +} + +/** @param int<-5, 5> $x */ +function castFloat($x, string $s, bool $b) { + if ((float) $x) { + assertType('int<-5, 5>', $x); + } else { + assertType('int<-5, 5>', $x); + } + + if ((float) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((float) $s) { + assertType('string', $s); + } else { + assertType("string", $s); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php new file mode 100644 index 0000000000..8ecf3438e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php @@ -0,0 +1,111 @@ +, numeric-string} $arr */ + public function nestedArrays(array $arr): void + { + // don't narrow when $arr contains recursive arrays + if (count($arr, COUNT_RECURSIVE) === 3) { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + + if (count($arr, COUNT_NORMAL) === 3) { + assertType("array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + + /** @param array{string, '', non-empty-string}|array $arr */ + public function mixedArrays(array $arr): void + { + if (count($arr, COUNT_NORMAL) === 3) { + assertType("non-empty-array", $arr); // could be array{string, '', non-empty-string}|non-empty-array + } else { + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + + public function arrayIntRangeSize(): void + { + $x = []; + if (rand(0,1)) { + $x[] = 'ab'; + } + if (rand(0,1)) { + $x[] = 'xy'; + } + + if (count($x) === 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/native-expressions.php b/tests/PHPStan/Analyser/nsrt/native-expressions.php new file mode 100644 index 0000000000..abe041686a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-expressions.php @@ -0,0 +1,56 @@ +|non-empty-string', $a); + assertNativeType('int|string', $a); + if (is_string($a)) { + assertType('non-empty-string', $a); + assertNativeType('string', $a); + } +} + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + assertType('non-empty-array', $this->array); + assertNativeType('array', $this->array); + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } + + /** + * @param array{a: 'b'} $a + * @return void + */ + public function doUnset(array $a){ + assertType("array{a: 'b'}", $a); + assertNativeType('array', $a); + unset($a['a']); + assertType("array{}", $a); + assertNativeType("array", $a); + } +} + diff --git a/tests/PHPStan/Analyser/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..6c216d23a5 --- /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('array{4, 5, 6}', $a); + assertNativeType('array', $a); + } + +} + +class PositiveInt +{ + + /** + * @param positive-int $i + * @return void + */ + public function doFoo(int $i): void + { + assertType('true', $i > 0); + assertType('false', $i <= 0); + assertNativeType('bool', $i > 0); + assertNativeType('bool', $i <= 0); + } + +} diff --git a/tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php b/tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types.php diff --git a/tests/PHPStan/Analyser/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/data/non-empty-array-key-type.php b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php similarity index 91% rename from tests/PHPStan/Analyser/data/non-empty-array-key-type.php rename to tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php index 90ae50bcd5..479fe40846 100644 --- a/tests/PHPStan/Analyser/data/non-empty-array-key-type.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php @@ -15,7 +15,7 @@ public function doFoo(array $items) assertType('array', $items); if (count($items) > 0) { - assertType('array&nonEmpty', $items); + assertType('non-empty-array', $items); foreach ($items as $i => $val) { assertType('(int|string)', $i); assertType('stdClass', $val); diff --git a/tests/PHPStan/Analyser/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-file-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-file-functions.php new file mode 100644 index 0000000000..794a17316b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-file-functions.php @@ -0,0 +1,194 @@ += 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..12ca4c3fac --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -0,0 +1,168 @@ + $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/data/nullsafe.php b/tests/PHPStan/Analyser/nsrt/nullsafe.php similarity index 95% rename from tests/PHPStan/Analyser/data/nullsafe.php rename to tests/PHPStan/Analyser/nsrt/nullsafe.php index fcb27c2ebd..ed4b00481a 100644 --- a/tests/PHPStan/Analyser/data/nullsafe.php +++ b/tests/PHPStan/Analyser/nsrt/nullsafe.php @@ -99,4 +99,11 @@ public function doDolor(?self $self) assertType('Nullsafe\Foo|null', $self?->nullableSelf); } + public function doNull(): void + { + $null = null; + assertType('null', $null?->foo); + assertType('null', $null?->doFoo()); + } + } diff --git a/tests/PHPStan/Analyser/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/data/offset-value-after-assign.php b/tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/offset-value-after-assign.php rename to tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php diff --git a/tests/PHPStan/Analyser/nsrt/openssl-cipher-iv-length-php7.php b/tests/PHPStan/Analyser/nsrt/openssl-cipher-iv-length-php7.php new file mode 100644 index 0000000000..72b8c41b87 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/openssl-cipher-iv-length-php7.php @@ -0,0 +1,24 @@ += 8.0 + +namespace OpensslCipherIvLengthPhp8; + +use function PHPStan\Testing\assertType; + +class OpensslCipher +{ + + /** + * @param 'aes-256-cbc'|'aes128'|'aes-128-cbc' $validAlgorithms + * @param 'aes-256-cbc'|'invalid' $validAndInvalidAlgorithms + */ + public function doFoo(string $s, $validAlgorithms, $validAndInvalidAlgorithms) + { + assertType('int', openssl_cipher_iv_length('aes-256-cbc')); + assertType('int', openssl_cipher_iv_length('AES-256-CBC')); + assertType('false', openssl_cipher_iv_length('unsupported')); + assertType('int|false', openssl_cipher_iv_length($s)); + assertType('int', openssl_cipher_iv_length($validAlgorithms)); + assertType('int|false', openssl_cipher_iv_length($validAndInvalidAlgorithms)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/openssl-cipher-key-length.php b/tests/PHPStan/Analyser/nsrt/openssl-cipher-key-length.php new file mode 100644 index 0000000000..c9081ae67c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/openssl-cipher-key-length.php @@ -0,0 +1,24 @@ += 8.2 + +namespace OpensslCipherKeyLength; + +use function PHPStan\Testing\assertType; + +class OpensslCipher +{ + + /** + * @param 'aes-256-cbc'|'aes128'|'aes-128-cbc' $validAlgorithms + * @param 'aes-256-cbc'|'invalid' $validAndInvalidAlgorithms + */ + public function doFoo(string $s, $validAlgorithms, $validAndInvalidAlgorithms) + { + assertType('int', openssl_cipher_key_length('aes-256-cbc')); + assertType('int', openssl_cipher_key_length('AES-256-CBC')); + assertType('false', openssl_cipher_key_length('unsupported')); + assertType('int|false', openssl_cipher_key_length($s)); + assertType('int', openssl_cipher_key_length($validAlgorithms)); + assertType('int|false', openssl_cipher_key_length($validAndInvalidAlgorithms)); + } + +} 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 @@ +hello); +} diff --git a/tests/PHPStan/Analyser/data/override-root-scope-variable.php b/tests/PHPStan/Analyser/nsrt/override-root-scope-variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/override-root-scope-variable.php rename to tests/PHPStan/Analyser/nsrt/override-root-scope-variable.php diff --git a/tests/PHPStan/Analyser/nsrt/overriding-phpdoc-type-of-protected-property.php b/tests/PHPStan/Analyser/nsrt/overriding-phpdoc-type-of-protected-property.php new file mode 100644 index 0000000000..3cfaa38285 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/overriding-phpdoc-type-of-protected-property.php @@ -0,0 +1,27 @@ +config); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/param-closure-this.php b/tests/PHPStan/Analyser/nsrt/param-closure-this.php new file mode 100644 index 0000000000..96f9fb8f15 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/param-closure-this.php @@ -0,0 +1,318 @@ + $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 @@ += 8.4 + +namespace PdoConnectPHP84; + +use function PHPStan\Testing\assertType; + +/** + * @param 'mysql:foo'|'pgsql:foo' $mysqlOrPgsql + * @param 'mysql:foo'|'foo:foo' $mysqlOrFoo + */ +function test( + string $string, + string $mysqlOrPgsql, + string $mysqlOrFoo, +) { + assertType('PDO\Mysql', \PDO::connect('mysql:foo')); + assertType('PDO\Firebird', \PDO::connect('firebird:foo')); + assertType('PDO\Dblib', \PDO::connect('dblib:foo')); + assertType('PDO\Odbc', \PDO::connect('odbc:foo')); + assertType('PDO\Pgsql', \PDO::connect('pgsql:foo')); + assertType('PDO\Sqlite', \PDO::connect('sqlite:foo')); + + assertType('PDO', \PDO::connect($string)); + assertType('PDO\Mysql|PDO\Pgsql', \PDO::connect($mysqlOrPgsql)); + assertType('PDO', \PDO::connect($mysqlOrFoo)); +} diff --git a/tests/PHPStan/Analyser/nsrt/pdo-prepare.php b/tests/PHPStan/Analyser/nsrt/pdo-prepare.php new file mode 100644 index 0000000000..72c4ab39c6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pdo-prepare.php @@ -0,0 +1,17 @@ +prepare('DELETE FROM log'); + assertType('(PDOStatement|false)', $logDeleteQuery); + } + +} diff --git a/tests/PHPStan/Analyser/data/phpdoc-in-closure-bind.php b/tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-in-closure-bind.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-global.php similarity index 85% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-global.php index 75ed167f90..83c2e56cb6 100644 --- a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php +++ b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-global.php @@ -22,9 +22,9 @@ function () { $double = doFoo(); assertType('float|int', $number); - assertType('float|int|(string&numeric)', $numeric); + assertType('float|int|numeric-string', $numeric); assertType('bool', $boolean); assertType('resource', $resource); - assertType('*NEVER*', $never); + assertType('never', $never); assertType('float', $double); }; diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-namespace.php similarity index 86% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-namespace.php index b7ff3c7fda..c13d96db75 100644 --- a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php +++ b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-namespace.php @@ -8,7 +8,6 @@ class Number {} class Numeric {} class Boolean {} class Resource {} -class Never {} class Double {} function () { @@ -21,9 +20,6 @@ function () { /** @var Numeric $numeric */ $numeric = doFoo(); - /** @var Never $never */ - $never = doFoo(); - /** @var Resource $resource */ $resource = doFoo(); @@ -34,6 +30,5 @@ function () { assertType('PhpdocPseudoTypesNamespace\Numeric', $numeric); assertType('PhpdocPseudoTypesNamespace\Boolean', $boolean); assertType('PhpdocPseudoTypesNamespace\Resource', $resource); - assertType('PhpdocPseudoTypesNamespace\Never', $never); assertType('PhpdocPseudoTypesNamespace\Double', $double); }; diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-override.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-override.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-override.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-override.php diff --git a/tests/PHPStan/Analyser/nsrt/phpunit-integration.php b/tests/PHPStan/Analyser/nsrt/phpunit-integration.php new file mode 100644 index 0000000000..d6e50cf0ff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/phpunit-integration.php @@ -0,0 +1,62 @@ +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 @@ +, non-empty-array, string>>', $locations); + assertType('non-empty-array, string>', $locations[0]); +}; diff --git a/tests/PHPStan/Analyser/nsrt/pre-dec.php b/tests/PHPStan/Analyser/nsrt/pre-dec.php new file mode 100644 index 0000000000..f73c003990 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pre-dec.php @@ -0,0 +1,23 @@ +', 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, 80599>', 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..210a467372 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -0,0 +1,112 @@ +generate())); + assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->generate(), $this->generate())); + + assertType('list}|string>|false', preg_split($pattern, $subject, $offset, $flags)); + assertType('list}|string>|false', preg_split("//", $subject, $offset, $flags)); + + assertType('non-empty-list}|string>|false', preg_split($pattern, "1-2-3", $offset, $flags)); + assertType('list}|string>|false', preg_split($pattern, $subject, -1, $flags)); + assertType('list|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("list|false", preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('list}>|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); + } + + /** + * @return 1|'17' + */ + private function generate(): int|string { + return (rand() % 2 === 0) ? 1 : "17"; + } + + /** + * @param non-empty-string $nonEmptySubject + */ + public function doWithNonEmptySubject(string $pattern, string $nonEmptySubject, int $offset, int $flags): void + { + assertType('non-empty-list|false', preg_split("//", $nonEmptySubject)); + + assertType('non-empty-list}|string>|false', preg_split($pattern, $nonEmptySubject, $offset, $flags)); + assertType('non-empty-list}|string>|false', preg_split("//", $nonEmptySubject, $offset, $flags)); + + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_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 @@ +', $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-phpdoc-tag-private-property.php b/tests/PHPStan/Analyser/nsrt/property-phpdoc-tag-private-property.php new file mode 100644 index 0000000000..48f6a99c5d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/property-phpdoc-tag-private-property.php @@ -0,0 +1,23 @@ += 8.2 + +namespace PropertyPhpDocTagPrivateProperty; + +use function PHPStan\Testing\assertType; + +/** + * @property non-empty-string $bar + * @property non-empty-string $baz + */ +class Foo +{ + + private string $bar; + + public string $baz; + +} + +function (Foo $foo): void { + assertType('string', $foo->bar); + assertType('non-empty-string', $foo->baz); +}; 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 91% rename from tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php rename to tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php index 036f9b996d..a2ae0bf2b7 100644 --- a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php +++ b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php @@ -18,7 +18,7 @@ public function doFoo() public function doBar(): void { - assertType('array', $this->doFoo()); + assertType('list', $this->doFoo()); } /** diff --git a/tests/PHPStan/Analyser/nsrt/pure-callable.php b/tests/PHPStan/Analyser/nsrt/pure-callable.php new file mode 100644 index 0000000000..39ef172288 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pure-callable.php @@ -0,0 +1,18 @@ +', 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/data/range-numeric-string.php b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php similarity index 80% rename from tests/PHPStan/Analyser/data/range-numeric-string.php rename to tests/PHPStan/Analyser/nsrt/range-numeric-string.php index faddec206b..bae424e559 100644 --- a/tests/PHPStan/Analyser/data/range-numeric-string.php +++ b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php @@ -16,7 +16,7 @@ public function doFoo( string $b ): void { - assertType('array', range($a, $b)); + assertType('list', range($a, $b)); } } diff --git a/tests/PHPStan/Analyser/nsrt/range-to-string.php b/tests/PHPStan/Analyser/nsrt/range-to-string.php new file mode 100644 index 0000000000..49bb179309 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/range-to-string.php @@ -0,0 +1,22 @@ + $i + * @param int<-10, 10> $ii + * @param int<0, 128> $maxlong + * @param int<0, 129> $toolong + */ + public function sayHello($i, $ii, $maxlong, $toolong): void + { + assertType("'10'|'5'|'6'|'7'|'8'|'9'", (string) $i); + assertType("'-1'|'-10'|'-2'|'-3'|'-4'|'-5'|'-6'|'-7'|'-8'|'-9'|'0'|'1'|'10'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", (string) $ii); + assertType("'0'|'1'|'10'|'100'|'101'|'102'|'103'|'104'|'105'|'106'|'107'|'108'|'109'|'11'|'110'|'111'|'112'|'113'|'114'|'115'|'116'|'117'|'118'|'119'|'12'|'120'|'121'|'122'|'123'|'124'|'125'|'126'|'127'|'128'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'2'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'3'|'30'|'31'|'32'|'33'|'34'|'35'|'36'|'37'|'38'|'39'|'4'|'40'|'41'|'42'|'43'|'44'|'45'|'46'|'47'|'48'|'49'|'5'|'50'|'51'|'52'|'53'|'54'|'55'|'56'|'57'|'58'|'59'|'6'|'60'|'61'|'62'|'63'|'64'|'65'|'66'|'67'|'68'|'69'|'7'|'70'|'71'|'72'|'73'|'74'|'75'|'76'|'77'|'78'|'79'|'8'|'80'|'81'|'82'|'83'|'84'|'85'|'86'|'87'|'88'|'89'|'9'|'90'|'91'|'92'|'93'|'94'|'95'|'96'|'97'|'98'|'99'", (string) $maxlong); + assertType("lowercase-string&numeric-string&uppercase-string", (string) $toolong); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/reflection-type.php b/tests/PHPStan/Analyser/nsrt/reflection-type.php new file mode 100644 index 0000000000..d390747fe6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflection-type.php @@ -0,0 +1,15 @@ +getType()); + assertType('ReflectionType|null', $reflectionFunctionAbstract->getReturnType()); + assertType('ReflectionType|null', $reflectionParameter->getType()); +} diff --git a/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php new file mode 100644 index 0000000000..d0a80299b2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php @@ -0,0 +1,81 @@ += 8.0 + +declare(strict_types=1); + +namespace Issue5511; + +use function PHPStan\Testing\assertType; + +#[\Attribute] +class Abc +{ +} + +/** + * @param string $str + * @param class-string $className + * @param class-string $genericClassName + */ +function testGetAttributes( + \ReflectionClass $reflectionClass, + \ReflectionMethod $reflectionMethod, + \ReflectionParameter $reflectionParameter, + \ReflectionProperty $reflectionProperty, + \ReflectionClassConstant $reflectionClassConstant, + \ReflectionFunction $reflectionFunction, + string $str, + string $className, + string $genericClassName +): void +{ + $classAll = $reflectionClass->getAttributes(); + $classAbc1 = $reflectionClass->getAttributes(Abc::class); + $classAbc2 = $reflectionClass->getAttributes(Abc::class, \ReflectionAttribute::IS_INSTANCEOF); + $classGCN = $reflectionClass->getAttributes($genericClassName); + $classCN = $reflectionClass->getAttributes($className); + $classStr = $reflectionClass->getAttributes($str); + $classNonsense = $reflectionClass->getAttributes("some random string"); + + assertType('list>', $classAll); + assertType('list>', $classAbc1); + assertType('list>', $classAbc2); + assertType('list>', $classGCN); + assertType('list>', $classCN); + assertType('list>', $classStr); + assertType('list>', $classNonsense); + + $methodAll = $reflectionMethod->getAttributes(); + $methodAbc = $reflectionMethod->getAttributes(Abc::class); + assertType('list>', $methodAll); + assertType('list>', $methodAbc); + + $paramAll = $reflectionParameter->getAttributes(); + $paramAbc = $reflectionParameter->getAttributes(Abc::class); + assertType('list>', $paramAll); + assertType('list>', $paramAbc); + + $propAll = $reflectionProperty->getAttributes(); + $propAbc = $reflectionProperty->getAttributes(Abc::class); + assertType('list>', $propAll); + assertType('list>', $propAbc); + + $constAll = $reflectionClassConstant->getAttributes(); + $constAbc = $reflectionClassConstant->getAttributes(Abc::class); + assertType('list>', $constAll); + assertType('list>', $constAbc); + + $funcAll = $reflectionFunction->getAttributes(); + $funcAbc = $reflectionFunction->getAttributes(Abc::class); + assertType('list>', $funcAll); + assertType('list>', $funcAbc); +} + +/** + * @param \ReflectionAttribute $ra + */ +function testNewInstance(\ReflectionAttribute $ra): void +{ + assertType('ReflectionAttribute', $ra); + $abc = $ra->newInstance(); + assertType(Abc::class, $abc); +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php new file mode 100644 index 0000000000..ed949a846f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php @@ -0,0 +1,88 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNonNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class KeepsPropertyNonNullable { + private readonly int $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class DontCoercePhpdocType { + /** @var int */ + private $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('mixed', $this->i); + } +} + +function getIntOrNull(): ?int { + if (rand(0, 1) === 0) { + return null; + } + return 1; +} + + +class KeepsPropertyNonNullable2 { + private int|float $i; + + public function __construct() + { + $this->i = getIntOrFloatOrNull(); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } +} + +function getIntOrFloatOrNull(): null|int|float { + if (rand(0, 1) === 0) { + return null; + } + + if (rand(0, 10) === 0) { + return 1.0; + } + return 1; +} + +class NarrowsNativeUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +function getInt(): int { + return 1; +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php new file mode 100644 index 0000000000..9618bc818f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +interface ObjectDataMapper +{ + /** + * @template OutType of object + * + * @param literal-string&class-string $class + * @param mixed $data + * + * @return OutType + * + * @throws \Exception + */ + public function map(string $class, $data): object; +} + +final class ApiProductController +{ + + protected ?SearchProductsVM $searchProductsVM = null; + + protected static ?SearchProductsVM $searchProductsVMStatic = null; + + public function search(ObjectDataMapper $dataMapper): void + { + $this->searchProductsVM = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', $this->searchProductsVM); + } + + public function searchStatic(ObjectDataMapper $dataMapper): void + { + self::$searchProductsVMStatic = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', self::$searchProductsVMStatic); + } +} + +class SearchProductsVM {} diff --git a/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php new file mode 100644 index 0000000000..979a2d7d91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php @@ -0,0 +1,111 @@ +pure() === 1) { + assertType('1', $this->pure()); + } + + if ($this->maybePure() === 1) { + assertType('1', $this->maybePure()); + } + + if ($this->impure() === 1) { + assertType('int', $this->impure()); + } + } + +} + +class FooStatic +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 1; + } + + public static function maybePure(): int + { + return 1; + } + + /** @phpstan-impure */ + public static function impure(): int + { + return rand(0, 1); + } + + public function test(): void + { + if (self::pure() === 1) { + assertType('1', self::pure()); + } + + if (self::maybePure() === 1) { + assertType('1', self::maybePure()); + } + + if (self::impure() === 1) { + assertType('int', self::impure()); + } + } + +} + +/** @phpstan-pure */ +function pure(): int +{ + return 1; +} + +function maybePure(): int +{ + return 1; +} + +/** @phpstan-impure */ +function impure(): int +{ + return rand(0, 1); +} + +function test(): void +{ + if (pure() === 1) { + assertType('1', pure()); + } + + if (maybePure() === 1) { + assertType('1', maybePure()); + } + + if (impure() === 1) { + assertType('int', impure()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php new file mode 100644 index 0000000000..8f03858767 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php @@ -0,0 +1,35 @@ += 8.4 + +namespace RememberReadOnlyConstructorInPropertyHookBodies; + +use function PHPStan\Testing\assertType; + +class User +{ + public string $name { + get { + assertType('1|2', $this->type); + return $this->name ; + } + set { + if (strlen($value) === 0) { + throw new ValueError("Name must be non-empty"); + } + assertType('1|2', $this->type); + $this->name = $value; + } + } + + private readonly int $type; + + public function __construct( + string $name + ) { + $this->name = $name; + if (rand(0,1)) { + $this->type = 1; + } else { + $this->type = 2; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php new file mode 100644 index 0000000000..55f8351fad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php @@ -0,0 +1,145 @@ += 8.2 + +namespace RememberReadOnlyConstructor; + +use LogicException; +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); + } +} + +class Foo { + public readonly int $readonly; + public int $writable; + + public function __construct() + { + $this->readonly = 5; + $this->writable = rand(0,1) ? 5 : 10; + } +} + +class DeepPropertyFetching { + public readonly ?Foo $prop; + + public function __construct() { + $this->prop = new Foo(); + if($this->prop->readonly != 5) { + throw new LogicException(); + } + if ($this->prop->writable != 5) { + throw new LogicException(); + } + + assertType(Foo::class, $this->prop); + assertType('5', $this->prop->readonly); + assertType('5', $this->prop->writable); + } + + public function doFoo() { + assertType(Foo::class, $this->prop); + assertType('5', $this->prop->readonly); + assertType('int', $this->prop->writable); + } +} diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php similarity index 91% rename from tests/PHPStan/Analyser/data/root-scope-maybe-defined.php rename to tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php index b8fa432f9b..800858e827 100644 --- a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php +++ b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php @@ -16,7 +16,7 @@ \PHPStan\Testing\assertType('1', $baz); } -\PHPStan\Testing\assertType('mixed', $baz); +\PHPStan\Testing\assertType('mixed~null', $baz); function () { \PHPStan\Testing\assertVariableCertainty(TrinaryLogic::createNo(), $foo); diff --git a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php new file mode 100644 index 0000000000..c618f6c8d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php @@ -0,0 +1,63 @@ += 8.0 + +declare(strict_types=1); + +namespace RoundFamilyTestPHP8StrictTypes; + +use function PHPStan\Testing\assertType; + +$maybeNull = null; +if (rand(0, 1)) { + $maybeNull = 1.0; +} + +// Round +assertType('float', round(123)); +assertType('float', round(123.456)); +assertType('float', round($_GET['foo'] / 60)); +assertType('*NEVER*', round('123')); +assertType('*NEVER*', round('123.456')); +assertType('*NEVER*', round(null)); +assertType('float', round($maybeNull)); +assertType('*NEVER*', round(true)); +assertType('*NEVER*', round(false)); +assertType('*NEVER*', round(new \stdClass)); +assertType('*NEVER*', round('')); +assertType('*NEVER*', round(array())); +assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); +assertType('float', round($_GET['foo'])); + +// Ceil +assertType('float', ceil(123)); +assertType('float', ceil(123.456)); +assertType('float', ceil($_GET['foo'] / 60)); +assertType('*NEVER*', ceil('123')); +assertType('*NEVER*', ceil('123.456')); +assertType('*NEVER*', ceil(null)); +assertType('float', ceil($maybeNull)); +assertType('*NEVER*', ceil(true)); +assertType('*NEVER*', ceil(false)); +assertType('*NEVER*', ceil(new \stdClass)); +assertType('*NEVER*', ceil('')); +assertType('*NEVER*', ceil(array())); +assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); +assertType('float', ceil($_GET['foo'])); + +// Floor +assertType('float', floor(123)); +assertType('float', floor(123.456)); +assertType('float', floor($_GET['foo'] / 60)); +assertType('*NEVER*', floor('123')); +assertType('*NEVER*', floor('123.456')); +assertType('*NEVER*', floor(null)); +assertType('float', floor($maybeNull)); +assertType('*NEVER*', floor(true)); +assertType('*NEVER*', floor(false)); +assertType('*NEVER*', floor(new \stdClass)); +assertType('*NEVER*', floor('')); +assertType('*NEVER*', floor(array())); +assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); +assertType('float', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php new file mode 100644 index 0000000000..54836b7623 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -0,0 +1,61 @@ += 8.0 + +namespace RoundFamilyTestPHP8; + +use function PHPStan\Testing\assertType; + +$maybeNull = null; +if (rand(0, 1)) { + $maybeNull = 1.0; +} + +// Round +assertType('float', round(123)); +assertType('float', round(123.456)); +assertType('float', round($_GET['foo'] / 60)); +assertType('float', round('123')); +assertType('float', round('123.456')); +assertType('float', round(null)); +assertType('float', round($maybeNull)); +assertType('float', round(true)); +assertType('float', round(false)); +assertType('*NEVER*', round(new \stdClass)); +assertType('*NEVER*', round('')); +assertType('*NEVER*', round(array())); +assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); +assertType('float', round($_GET['foo'])); + +// Ceil +assertType('float', ceil(123)); +assertType('float', ceil(123.456)); +assertType('float', ceil($_GET['foo'] / 60)); +assertType('float', ceil('123')); +assertType('float', ceil('123.456')); +assertType('float', ceil(null)); +assertType('float', ceil($maybeNull)); +assertType('float', ceil(true)); +assertType('float', ceil(false)); +assertType('*NEVER*', ceil(new \stdClass)); +assertType('*NEVER*', ceil('')); +assertType('*NEVER*', ceil(array())); +assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); +assertType('float', ceil($_GET['foo'])); + +// Floor +assertType('float', floor(123)); +assertType('float', floor(123.456)); +assertType('float', floor($_GET['foo'] / 60)); +assertType('float', floor('123')); +assertType('float', floor('123.456')); +assertType('float', floor(null)); +assertType('float', floor($maybeNull)); +assertType('float', floor(true)); +assertType('float', floor(false)); +assertType('*NEVER*', floor(new \stdClass)); +assertType('*NEVER*', floor('')); +assertType('*NEVER*', floor(array())); +assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); +assertType('float', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/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-function-method.php b/tests/PHPStan/Analyser/nsrt/scope-function-method.php new file mode 100644 index 0000000000..54d127aaa3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/scope-function-method.php @@ -0,0 +1,31 @@ +getFunction(); + if ($function === null) { + return; + } + + assertType(PhpFunctionFromParserNodeReflection::class, $function); + + if ($function->isMethodOrPropertyHook()) { + assertType(PhpMethodFromParserNodeReflection::class, $function); + } else { + assertType(PhpFunctionFromParserNodeReflection::class . '~' . PhpMethodFromParserNodeReflection::class, $function); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/scope-generalization.php b/tests/PHPStan/Analyser/nsrt/scope-generalization.php new file mode 100644 index 0000000000..5a183e1d1c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/scope-generalization.php @@ -0,0 +1,35 @@ + */ + $foo = []; + for ($i = 0; $i < 3; $i++) { + array_push($foo, 'foo'); + } + assertType('non-empty-array', $foo); +} + +function loopRemovesAccessory(): void +{ + /** @var non-empty-array */ + $foo = []; + for ($i = 0; $i < 3; $i++) { + array_pop($foo); + } + assertType('array', $foo); +} + +function closureRemovesAccessoryOfReferenceParameter(): void +{ + /** @var non-empty-array */ + $foo = []; + static function () use (&$foo) { + assertType('array', $foo); + array_pop($foo); + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/self-out.php b/tests/PHPStan/Analyser/nsrt/self-out.php new file mode 100644 index 0000000000..fa623d2d5a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/self-out.php @@ -0,0 +1,108 @@ + + */ + private array $data; + /** + * @param T $data + */ + public function __construct($data) { + $this->data = [$data]; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function addData($data) { + /** @var self $this */ + $this->data []= $data; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function setData($data) { + /** @var self $this */ + $this->data = [$data]; + } + /** + * @return ($this is a ? void : never) + */ + public function test(): void { + } + + /** + * @phpstan-self-out self + */ + public static function selfOutWithStaticMethod(): void + { + + } +} + +/** + * @template T + * @extends a + */ +class b extends a { + /** + * @param T $data + */ + public function __construct($data) { + parent::__construct($data); + } +} + +function () { + $i = new a(123); + // OK - $i is a<123> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->addData(321); + // OK - $i is a<123|321> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->setData("test"); + // IfThisIsMismatch - Class is not a as required + assertType('SelfOut\\a<\'test\'>', $i); + assertType('never', $i->test()); +}; + +function () { + $i = new b(123); + assertType('SelfOut\\b', $i); + + $i->addData(321); + assertType('SelfOut\\a', $i); + + $i->addData(random_bytes(3)); + assertType('SelfOut\\a', $i); + + $i->setData(true); + assertType('SelfOut\\a', $i); + + $i->selfOutWithStaticMethod(); + assertType('SelfOut\\a', $i); +}; diff --git a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php new file mode 100644 index 0000000000..f7513c6045 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -0,0 +1,673 @@ + 'bar']; + settype($x, 'string'); + assertType('*ERROR*', $x); + + // array to int + $x = []; + settype($x, 'int'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'int'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'int'); + assertType('1', $x); + + // array to integer + $x = []; + settype($x, 'integer'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'integer'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'integer'); + assertType('1', $x); + + // array to float + $x = []; + settype($x, 'float'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'float'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'float'); + assertType('1.0', $x); + + // array to double + $x = []; + settype($x, 'double'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'double'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'double'); + assertType('1.0', $x); + + // array to bool + $x = []; + settype($x, 'bool'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'bool'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'bool'); + assertType('true', $x); + + // array to boolean + $x = []; + settype($x, 'boolean'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'boolean'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'boolean'); + assertType('true', $x); + + // array to array + $x = []; + settype($x, 'array'); + assertType('array{}', $x); + + $x = ['foo']; + settype($x, 'array'); + assertType("array{'foo'}", $x); + + $x = ['foo' => 'bar']; + settype($x, 'array'); + assertType("array{foo: 'bar'}", $x); + + // array to object + $x = []; + settype($x, 'object'); + assertType('stdClass', $x); + + $x = ['foo']; + settype($x, 'object'); + assertType("stdClass", $x); + + $x = ['foo' => 'bar']; + settype($x, 'object'); + assertType("stdClass", $x); + + // array to null + $x = []; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo']; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo' => 'bar']; + settype($x, 'null'); + assertType('null', $x); + + // object to string + $x = new stdClass(); + settype($x, 'string'); + assertType('*ERROR*', $x); + + // object to int + $x = new stdClass(); + settype($x, 'int'); + assertType('*ERROR*', $x); + + // object to integer + $x = new stdClass(); + settype($x, 'integer'); + assertType('*ERROR*', $x); + + // object to float + $x = new stdClass(); + settype($x, 'float'); + assertType('*ERROR*', $x); + + // object to double + $x = new stdClass(); + settype($x, 'double'); + assertType('*ERROR*', $x); + + // object to bool + $x = new stdClass(); + settype($x, 'bool'); + assertType('true', $x); + + // object to boolean + $x = new stdClass(); + settype($x, 'boolean'); + assertType('true', $x); + + // object to array + $x = new stdClass(); + settype($x, 'array'); + assertType('array', $x); + + // object to object + $x = new stdClass(); + settype($x, 'object'); + assertType('stdClass', $x); + + // object to null + $x = new stdClass(); + settype($x, 'null'); + assertType('null', $x); + + // null to string + $x = null; + settype($x, 'string'); + assertType("''", $x); + + // null to int + $x = null; + settype($x, 'int'); + assertType('0', $x); + + // null to integer + $x = null; + settype($x, 'integer'); + assertType('0', $x); + + // null to float + $x = null; + settype($x, 'float'); + assertType('0.0', $x); + + // null to double + $x = null; + settype($x, 'double'); + assertType('0.0', $x); + + // null to bool + $x = null; + settype($x, 'bool'); + assertType('false', $x); + + // null to boolean + $x = null; + settype($x, 'boolean'); + assertType('false', $x); + + // null to array + $x = null; + settype($x, 'array'); + assertType('array{}', $x); + + // null to object + $x = null; + settype($x, 'object'); + assertType('stdClass', $x); + + // null to null + $x = null; + settype($x, 'null'); + assertType('null', $x); + + // Mixed to non-constant. + settype($value, $castTo); + assertType("array|bool|float|int|stdClass|string|null", $value); +} diff --git a/tests/PHPStan/Analyser/data/shadowed-trait-methods.php b/tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/shadowed-trait-methods.php rename to tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php diff --git a/tests/PHPStan/Analyser/nsrt/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php new file mode 100644 index 0000000000..6b699e598a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/shuffle.php @@ -0,0 +1,141 @@ +', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + + public function normalArrays2(array $arr): void + { + /** @var non-empty-array $arr */ + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + + public function normalArrays3(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr)) { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + } + + public function normalArrays4(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + } + + public function constantArrays1(array $arr): void + { + $arr = []; + shuffle($arr); + assertType('array{}', $arr); + assertNativeType('array{}', $arr); + assertType('array{}', array_keys($arr)); + assertType('array{}', array_values($arr)); + } + + public function constantArrays2(array $arr): void + { + /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ + shuffle($arr); + assertType('list<1|2|3>', $arr); + assertNativeType('list', $arr); + assertType('list<0|1|2>', array_keys($arr)); + assertType('list<1|2|3>', array_values($arr)); + } + + public function constantArrays3(array $arr): void + { + $arr = [1, 2, 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays4(array $arr): void + { + $arr = ['a' => 1, 'b' => 2, 'c' => 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays5(array $arr): void + { + $arr = [0 => 1, 3 => 2, 42 => 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays6(array $arr): void + { + /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ + shuffle($arr); + assertType('non-empty-list<1|2|3|4>', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list<0|1>', array_keys($arr)); + assertType('non-empty-list<1|2|3|4>', array_values($arr)); + } + + public function mixed($arr): void + { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + + public function subtractedArray($arr): void + { + if (is_array($arr)) { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } else { + shuffle($arr); + assertType('*ERROR*', $arr); + assertNativeType('*ERROR*', $arr); + assertType('list', array_keys($arr)); + assertType('list', array_values($arr)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/simplexml.php b/tests/PHPStan/Analyser/nsrt/simplexml.php new file mode 100644 index 0000000000..69cea239d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/simplexml.php @@ -0,0 +1,92 @@ +item); + foreach ($data->item as $item) { + assertType('SimpleXMLElement', $item); + assertType('SimpleXMLElement|null', $item['name']); + } + } + +} + +class Bar extends SimpleXMLElement +{ + + public function getAddressByGps() + { + /** @var self|null $data */ + $data = doFoo(); + + if ($data === null) { + return; + } + + assertType('(SimpleXMLIteratorBug\Bar|null)', $data->item); + foreach ($data->item as $item) { + assertType(self::class, $item); + assertType('SimpleXMLIteratorBug\Bar|null', $item['name']); + } + } + +} + +class Baz +{ + + public function getAddressByGps() + { + /** @var Bar|null $data */ + $data = doFoo(); + + if ($data === null) { + return; + } + + assertType('(SimpleXMLIteratorBug\Bar|null)', $data->item); + foreach ($data->item as $item) { + assertType(Bar::class, $item); + assertType('SimpleXMLIteratorBug\Bar|null', $item['name']); + } + } + +} + +class AsXML +{ + + public function asXML(): void + { + $element = new SimpleXMLElement(''); + + assertType('string|false', $element->asXML()); + + assertType('bool', $element->asXML('/tmp/foo.xml')); + } + + public function saveXML(): void + { + $element = new SimpleXMLElement(''); + + assertType('string|false', $element->saveXML()); + + assertType('bool', $element->saveXML('/tmp/foo.xml')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/sizeof-php8.php b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php new file mode 100644 index 0000000000..a681a9f906 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php @@ -0,0 +1,63 @@ += 8.0 + +namespace sizeof_php8; + +use function PHPStan\Testing\assertType; + + +class Sizeof +{ + /** + * @param int[] $ints + */ + function doFoo1(array $ints): string + { + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + } + + /** + * @param int[] $ints + */ + function doFoo2(array $ints): string + { + if (sizeof($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + } + + function doFoo3(array $arr): string + { + if (0 != count($arr)) { + assertType('non-empty-array', $arr); + } + return ""; + } + + function doFoo4(array $arr): string + { + if (0 != sizeof($arr)) { + assertType('non-empty-array', $arr); + } + return ""; + } + + function doFoo5(array $arr): void + { + if ([] != $arr) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); + } + + function doFoo6(array $arr): void + { + if ($arr != []) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); + } +} diff --git a/tests/PHPStan/Analyser/data/sizeof.php b/tests/PHPStan/Analyser/nsrt/sizeof.php similarity index 81% rename from tests/PHPStan/Analyser/data/sizeof.php rename to tests/PHPStan/Analyser/nsrt/sizeof.php index f569d8902a..eec773844c 100644 --- a/tests/PHPStan/Analyser/data/sizeof.php +++ b/tests/PHPStan/Analyser/nsrt/sizeof.php @@ -1,4 +1,4 @@ -|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/snmp.php b/tests/PHPStan/Analyser/nsrt/snmp.php new file mode 100644 index 0000000000..fe3cfd7b59 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/snmp.php @@ -0,0 +1,18 @@ +get('SNMPv2-MIB::sysContact.0'); + assertType('string|false', $result); + + $result = $snmp->get(['SNMPv2-MIB::sysContact.0', 'SNMPv2-MIB::sysDescr.0']); + assertType('array|false', $result); + } +} 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/data/splfixedarray-iterator-types.php b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php similarity index 88% rename from tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php rename to tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php index 4c512bea06..10b3859354 100644 --- a/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php +++ b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php @@ -1,5 +1,7 @@ = 8.2 + +namespace StandaloneTypes; + +use function PHPStan\Testing\assertType; + +function foo(): null { + return null; +} +function bar(): false { + return false; +} + +class standalone { + public false $f = false; + public null $n = null; + + function foo(): null { + return null; + } + function bar(): false { + return false; + } +} + +function takesNull(null $n) { + assertType('null', $n); +} + +function takesFalse(false $f) { + assertType('false', $f); +} + + +function doFoo() { + assertType('null', foo()); + assertType('false', bar()); + + $s = new standalone(); + + assertType('null', $s->foo()); + assertType('false', $s->bar()); + + assertType('null', $s->n); + assertType('false', $s->f); +} diff --git a/tests/PHPStan/Analyser/nsrt/static-has-method.php b/tests/PHPStan/Analyser/nsrt/static-has-method.php new file mode 100644 index 0000000000..b00640bc91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/static-has-method.php @@ -0,0 +1,21 @@ +retStaticConst()); + assertType('bool', X::retStaticConst()); + assertType('*ERROR*', $clUnioned->retStaticConst()); // should be bool|int https://github.com/phpstan/phpstan/issues/11687 + + assertType('int', A::retStaticConst(...)()); + assertType('2', B::retStaticConst(...)()); + assertType('2', self::retStaticConst(...)()); + assertType('2', static::retStaticConst(...)()); + assertType('int', parent::retStaticConst(...)()); + assertType('2', $this->retStaticConst(...)()); + assertType('bool', X::retStaticConst(...)()); + assertType('mixed', $clUnioned->retStaticConst(...)()); // should be bool|int https://github.com/phpstan/phpstan/issues/11687 + + assertType('StaticLateBinding\A', A::retStatic()); + assertType('StaticLateBinding\B', B::retStatic()); + assertType('static(StaticLateBinding\B)', self::retStatic()); + assertType('static(StaticLateBinding\B)', static::retStatic()); + assertType('static(StaticLateBinding\B)', parent::retStatic()); + assertType('static(StaticLateBinding\B)', $this->retStatic()); + assertType('bool', X::retStatic()); + assertType('bool|StaticLateBinding\A|StaticLateBinding\X', $clUnioned::retStatic()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 + + assertType('StaticLateBinding\A', A::retStatic(...)()); + assertType('StaticLateBinding\B', B::retStatic(...)()); + assertType('static(StaticLateBinding\B)', self::retStatic(...)()); + assertType('static(StaticLateBinding\B)', static::retStatic(...)()); + assertType('static(StaticLateBinding\B)', parent::retStatic(...)()); + assertType('static(StaticLateBinding\B)', $this->retStatic(...)()); + assertType('bool', X::retStatic(...)()); + assertType('mixed', $clUnioned::retStatic(...)()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 + + assertType('static(StaticLateBinding\B)', A::retNonStatic()); + assertType('static(StaticLateBinding\B)', B::retNonStatic()); + assertType('static(StaticLateBinding\B)', self::retNonStatic()); + assertType('static(StaticLateBinding\B)', static::retNonStatic()); + assertType('static(StaticLateBinding\B)', parent::retNonStatic()); + assertType('static(StaticLateBinding\B)', $this->retNonStatic()); + assertType('bool', X::retNonStatic()); + assertType('*ERROR*', $clUnioned->retNonStatic()); // should be bool|static(StaticLateBinding\B) https://github.com/phpstan/phpstan/issues/11687 + + A::outStaticConst($v); + assertType('int', $v); + B::outStaticConst($v); + assertType('2', $v); + self::outStaticConst($v); + assertType('2', $v); + static::outStaticConst($v); + assertType('2', $v); + parent::outStaticConst($v); + assertType('int', $v); + $this->outStaticConst($v); + assertType('2', $v); + X::outStaticConst($v); + assertType('bool', $v); + $clUnioned->outStaticConst($v); + assertType('bool', $v); // should be bool|int + } +} + +class X +{ + public static function retStaticConst(): bool + { + return false; + } + + /** + * @param-out bool $out + */ + public static function outStaticConst(&$out): void + { + $out = false; + } + + public static function retStatic(): bool + { + return false; + } + + public function retNonStatic(): bool + { + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/static-methods.php b/tests/PHPStan/Analyser/nsrt/static-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-methods.php rename to tests/PHPStan/Analyser/nsrt/static-methods.php diff --git a/tests/PHPStan/Analyser/data/static-properties.php b/tests/PHPStan/Analyser/nsrt/static-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-properties.php rename to tests/PHPStan/Analyser/nsrt/static-properties.php diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php new file mode 100644 index 0000000000..ebdbd8054d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -0,0 +1,105 @@ += 7.3 + +namespace StrCasingReturnType; + +use function PHPStan\Testing\assertType; + +class Foo { + /** + * @param numeric-string $numericS + * @param non-empty-string $nonE + * @param lowercase-string $lowercaseS + * @param literal-string $literal + * @param 'foo'|'Foo' $edgeUnion + * @param MB_CASE_UPPER|MB_CASE_LOWER|MB_CASE_TITLE|MB_CASE_FOLD|MB_CASE_UPPER_SIMPLE|MB_CASE_LOWER_SIMPLE|MB_CASE_TITLE_SIMPLE|MB_CASE_FOLD_SIMPLE $caseMode + * @param 'aKV'|'hA'|'AH'|'K'|'KV'|'RNKV' $kanaMode + * @param mixed $mixed + */ + public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMode, $kanaMode, $mixed) { + assertType("'abc'", strtolower('ABC')); + assertType("'ABC'", strtoupper('abc')); + assertType("'abc'", mb_strtolower('ABC')); + assertType("'ABC'", mb_strtoupper('abc')); + assertType("'abc'", mb_strtolower('ABC', 'UTF-8')); + assertType("'ABC'", mb_strtoupper('abc', 'UTF-8')); + assertType("'abc'", mb_strtolower('Abc')); + assertType("'ABC'", mb_strtoupper('Abc')); + assertType("'aBC'", lcfirst('ABC')); + assertType("'Abc'", ucfirst('abc')); + assertType("'Hello World'", ucwords('hello world')); + assertType("'Hello|World'", ucwords('hello|world', "|")); + assertType("'ČESKÁ REPUBLIKA'", mb_convert_case('Česká republika', MB_CASE_UPPER)); + assertType("'česká republika'", mb_convert_case('Česká republika', MB_CASE_LOWER)); + assertType("non-falsy-string", mb_convert_case('Česká republika', $mixed)); + assertType("'ČESKÁ REPUBLIKA'|'Česká Republika'|'česká republika'", mb_convert_case('Česká republika', $caseMode)); + assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字')); + assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字', 'aKV')); + assertType("'Abc123アイウガギグバビブ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字', 'hA')); + assertType("'Abc123アガば漢'|'Abc123あか゛ば漢'|'Abc123アカ゛ば漢'|'Abc123アガば漢'|'Abc123アガバ漢'", mb_convert_kana('Abc123アガば漢', $kanaMode)); + assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); + + assertType("lowercase-string&numeric-string", strtolower($numericS)); + assertType("numeric-string&uppercase-string", strtoupper($numericS)); + assertType("lowercase-string&numeric-string", mb_strtolower($numericS)); + assertType("numeric-string&uppercase-string", mb_strtoupper($numericS)); + assertType("numeric-string", lcfirst($numericS)); + assertType("numeric-string", ucfirst($numericS)); + assertType("numeric-string", ucwords($numericS)); + assertType("numeric-string&uppercase-string", mb_convert_case($numericS, MB_CASE_UPPER)); + assertType("lowercase-string&numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); + assertType("numeric-string", mb_convert_case($numericS, $mixed)); + assertType("numeric-string", mb_convert_kana($numericS)); + assertType("numeric-string", mb_convert_kana($numericS, $mixed)); + + assertType("lowercase-string&non-empty-string", strtolower($nonE)); + assertType("non-empty-string&uppercase-string", strtoupper($nonE)); + assertType("lowercase-string&non-empty-string", mb_strtolower($nonE)); + assertType("non-empty-string&uppercase-string", mb_strtoupper($nonE)); + assertType("non-empty-string", lcfirst($nonE)); + assertType("non-empty-string", ucfirst($nonE)); + assertType("non-empty-string", ucwords($nonE)); + assertType("non-empty-string&uppercase-string", mb_convert_case($nonE, MB_CASE_UPPER)); + assertType("lowercase-string&non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); + assertType("non-empty-string", mb_convert_case($nonE, $mixed)); + assertType("non-empty-string", mb_convert_kana($nonE)); + assertType("non-empty-string", mb_convert_kana($nonE, $mixed)); + + assertType("lowercase-string", strtolower($literal)); + assertType("uppercase-string", strtoupper($literal)); + assertType("lowercase-string", mb_strtolower($literal)); + assertType("uppercase-string", mb_strtoupper($literal)); + assertType("string", lcfirst($literal)); + assertType("string", ucfirst($literal)); + assertType("string", ucwords($literal)); + assertType("uppercase-string", mb_convert_case($literal, MB_CASE_UPPER)); + assertType("lowercase-string", mb_convert_case($literal, MB_CASE_LOWER)); + assertType("string", mb_convert_case($literal, $mixed)); + assertType("string", mb_convert_kana($literal)); + assertType("string", mb_convert_kana($literal, $mixed)); + + assertType("lowercase-string", strtolower($lowercaseS)); + assertType("uppercase-string", strtoupper($lowercaseS)); + assertType("lowercase-string", mb_strtolower($lowercaseS)); + assertType("uppercase-string", mb_strtoupper($lowercaseS)); + assertType("lowercase-string", lcfirst($lowercaseS)); + assertType("string", ucfirst($lowercaseS)); + assertType("string", ucwords($lowercaseS)); + assertType("uppercase-string", mb_convert_case($lowercaseS, MB_CASE_UPPER)); + assertType("lowercase-string", mb_convert_case($lowercaseS, MB_CASE_LOWER)); + assertType("string", mb_convert_case($lowercaseS, $mixed)); + assertType("lowercase-string", mb_convert_case($lowercaseS, rand(0, 1) ? MB_CASE_LOWER : MB_CASE_LOWER_SIMPLE)); + assertType("string", mb_convert_kana($lowercaseS)); + assertType("string", mb_convert_kana($lowercaseS, $mixed)); + + assertType("'foo'", lcfirst($edgeUnion)); + } + + public function foo() { + // invalid char conversions still lead to non-falsy-string + assertType("lowercase-string&non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); + // valid char sequence, but not support non ASCII / UTF-8 encodings + assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'SJIS-win')); + // invalid UTF-8 sequence + assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'UTF-8')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/str-shuffle.php b/tests/PHPStan/Analyser/nsrt/str-shuffle.php new file mode 100644 index 0000000000..37aa768525 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/str-shuffle.php @@ -0,0 +1,19 @@ += 8.3 + +declare(strict_types = 1); + +namespace StrDecrementFunctionReturn; + +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-string $s + */ +function foo(string $s): void +{ + assertType('non-empty-string', str_decrement($s)); + assertType('*NEVER*', str_decrement('')); + assertType('*NEVER*', str_decrement('0')); + assertType('*NEVER*', str_decrement('0.0')); + assertType('*NEVER*', str_decrement('1.0')); + assertType('*NEVER*', str_decrement('a')); + assertType('*NEVER*', str_decrement('A')); + assertType('*NEVER*', str_decrement('=')); + assertType('*NEVER*', 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..a2c843390a --- /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('*NEVER*', str_increment('')); + assertType('*NEVER*', str_increment('=')); + assertType('*NEVER*', str_increment('0.0')); + assertType('*NEVER*', str_increment('1.0')); + assertType('*NEVER*', 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/strrev.php b/tests/PHPStan/Analyser/nsrt/strrev.php new file mode 100644 index 0000000000..2029c56df8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strrev.php @@ -0,0 +1,37 @@ +', $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/subtracted.php b/tests/PHPStan/Analyser/nsrt/subtracted.php new file mode 100644 index 0000000000..0bb82c342c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/subtracted.php @@ -0,0 +1,31 @@ +', $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/data/ternary-specified-types.php b/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/ternary-specified-types.php rename to tests/PHPStan/Analyser/nsrt/ternary-specified-types.php diff --git a/tests/PHPStan/Analyser/nsrt/this-subtractable.php b/tests/PHPStan/Analyser/nsrt/this-subtractable.php new file mode 100644 index 0000000000..a3d9a57554 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/this-subtractable.php @@ -0,0 +1,109 @@ +returnStatic(); + assertType('static(ThisSubtractable\Foo)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('static(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz))', $s); + } else { + assertType('(static(ThisSubtractable\Foo)&ThisSubtractable\Bar)|(static(ThisSubtractable\Foo)&ThisSubtractable\Baz)', $s); + } + + assertType('static(ThisSubtractable\Foo)', $s); + } + + public function doBaz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Lorem && !$s instanceof Ipsum) { + assertType('ThisSubtractable\Foo', $s); + } else { + assertType('(ThisSubtractable\Foo&ThisSubtractable\Ipsum)|(ThisSubtractable\Foo&ThisSubtractable\Lorem)', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)', $s); + } else { + assertType('ThisSubtractable\Bar|ThisSubtractable\Baz', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazzz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + if (!method_exists($s, 'test123', $s)) { + return; + } + + assertType('ThisSubtractable\Foo&hasMethod(test123)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)&hasMethod(test123)', $s); + } else { + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))', $s); + } + + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))|(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)&hasMethod(test123))', $s); + } + + /** + * @return static + */ + public function returnStatic() + { + return $this; + } + +} + +class Bar extends Foo +{ + +} + +class Baz extends Foo +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Analyser/data/throw-expr.php b/tests/PHPStan/Analyser/nsrt/throw-expr.php similarity index 84% rename from tests/PHPStan/Analyser/data/throw-expr.php rename to tests/PHPStan/Analyser/nsrt/throw-expr.php index 581e8b1d3e..2893fe4ee7 100644 --- a/tests/PHPStan/Analyser/data/throw-expr.php +++ b/tests/PHPStan/Analyser/nsrt/throw-expr.php @@ -15,7 +15,7 @@ public function doFoo(bool $b): void public function doBar(): void { - assertType('*NEVER*', throw new \Exception()); + assertType('never', throw new \Exception()); } } diff --git a/tests/PHPStan/Analyser/data/throw-points/and.php b/tests/PHPStan/Analyser/nsrt/throw-points/and.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/and.php rename to tests/PHPStan/Analyser/nsrt/throw-points/and.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array.php b/tests/PHPStan/Analyser/nsrt/throw-points/array.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign-op.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign-op.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign.php diff --git a/tests/PHPStan/Analyser/data/throw-points/do-while.php b/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/do-while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/do-while.php diff --git a/tests/PHPStan/Analyser/data/throw-points/for.php b/tests/PHPStan/Analyser/nsrt/throw-points/for.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/for.php rename to tests/PHPStan/Analyser/nsrt/throw-points/for.php diff --git a/tests/PHPStan/Analyser/data/throw-points/foreach.php b/tests/PHPStan/Analyser/nsrt/throw-points/foreach.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/foreach.php rename to tests/PHPStan/Analyser/nsrt/throw-points/foreach.php diff --git a/tests/PHPStan/Analyser/data/throw-points/func-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/func-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/func-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/func-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/if.php b/tests/PHPStan/Analyser/nsrt/throw-points/if.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/if.php rename to tests/PHPStan/Analyser/nsrt/throw-points/if.php diff --git a/tests/PHPStan/Analyser/data/throw-points/method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/or.php b/tests/PHPStan/Analyser/nsrt/throw-points/or.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/or.php rename to tests/PHPStan/Analyser/nsrt/throw-points/or.php diff --git a/tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/property-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/property-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/static-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/static-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/static-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/switch.php b/tests/PHPStan/Analyser/nsrt/throw-points/switch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/switch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/switch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/throw.php b/tests/PHPStan/Analyser/nsrt/throw-points/throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/throw.php rename to tests/PHPStan/Analyser/nsrt/throw-points/throw.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php similarity index 91% rename from tests/PHPStan/Analyser/data/throw-points/try-catch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php index 57faddd9bb..87c0be66eb 100644 --- a/tests/PHPStan/Analyser/data/throw-points/try-catch.php +++ b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php @@ -65,18 +65,18 @@ function (): void { $bar = 1; maybeThrows(); } catch (\InvalidArgumentException $e) { - assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); assertType('1|2', $foo); - assertVariableCertainty(TrinaryLogic::createNo(), $bar); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); assertVariableCertainty(TrinaryLogic::createNo(), $baz); } catch (\RuntimeException $e) { assertVariableCertainty(TrinaryLogic::createNo(), $foo); - assertVariableCertainty(TrinaryLogic::createNo(), $bar); - assertVariableCertainty(TrinaryLogic::createYes(), $baz); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); + assertVariableCertainty(TrinaryLogic::createMaybe(), $baz); assertType('1|2', $baz); } catch (\Throwable $e) { - assertType('Throwable~InvalidArgumentException|RuntimeException', $e); + assertType('Throwable~(InvalidArgumentException|RuntimeException)', $e); assertVariableCertainty(TrinaryLogic::createNo(), $foo); assertVariableCertainty(TrinaryLogic::createYes(), $bar); assertVariableCertainty(TrinaryLogic::createNo(), $baz); @@ -99,7 +99,7 @@ function (): void { throw new \InvalidArgumentException(); } catch (\InvalidArgumentException $e) { assertType('1', $foo); - assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); } }; diff --git a/tests/PHPStan/Analyser/data/throw-points/variable.php b/tests/PHPStan/Analyser/nsrt/throw-points/variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/variable.php rename to tests/PHPStan/Analyser/nsrt/throw-points/variable.php diff --git a/tests/PHPStan/Analyser/data/throw-points/while.php b/tests/PHPStan/Analyser/nsrt/throw-points/while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/while.php diff --git a/tests/PHPStan/Analyser/nsrt/trait-type-alias.php b/tests/PHPStan/Analyser/nsrt/trait-type-alias.php new file mode 100644 index 0000000000..b83fe210fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/trait-type-alias.php @@ -0,0 +1,54 @@ += 8.0 + +declare(strict_types = 1); + +namespace TriggerErrorPhp8; + +use function PHPStan\Testing\assertType; + +$errorLevels = [E_USER_DEPRECATED, E_USER_ERROR, E_USER_NOTICE, E_USER_WARNING, E_NOTICE, E_WARNING]; + +assertType('true', trigger_error('bar')); +assertType('true', trigger_error('bar', $errorLevels[0])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[1])); +assertType('true', trigger_error('bar', $errorLevels[2])); +assertType('true', trigger_error('bar', $errorLevels[3])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[4])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[5])); diff --git a/tests/PHPStan/Analyser/nsrt/trim.php b/tests/PHPStan/Analyser/nsrt/trim.php new file mode 100644 index 0000000000..51908b0995 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/trim.php @@ -0,0 +1,44 @@ + $constantOrFooClass + * @param string $string + */ + public function doTrim($foo, $fooOrBar, $constantOrFooClass, $string): void + { + assertType('string', trim($string, $foo)); + assertType('string', ltrim($string, $foo)); + assertType('string', rtrim($string, $foo)); + + assertType('lowercase-string', trim($foo, $string)); + assertType('lowercase-string', ltrim($foo, $string)); + assertType('lowercase-string', rtrim($foo, $string)); + assertType('\'\'|\'foo\'', trim($foo, $fooOrBar)); + assertType('\'\'|\'foo\'', ltrim($foo, $fooOrBar)); + assertType('\'\'|\'foo\'', rtrim($foo, $fooOrBar)); + + assertType('lowercase-string', trim($fooOrBar, $string)); + assertType('lowercase-string', ltrim($fooOrBar, $string)); + assertType('lowercase-string', rtrim($fooOrBar, $string)); + assertType('\'\'|\'bar\'', trim($fooOrBar, $foo)); + assertType('\'\'|\'bar\'', ltrim($fooOrBar, $foo)); + assertType('\'\'|\'bar\'', rtrim($fooOrBar, $foo)); + + assertType('string', trim($constantOrFooClass, '\\')); + assertType('string', ltrim($constantOrFooClass, '\\')); + assertType('string', rtrim($constantOrFooClass, '\\')); + assertType('string', trim($constantOrFooClass, '\\')); + assertType('string', ltrim($constantOrFooClass, '\\')); + assertType('string', rtrim($constantOrFooClass, '\\')); + } + +} diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/nsrt/type-aliases.php similarity index 94% rename from tests/PHPStan/Analyser/data/type-aliases.php rename to tests/PHPStan/Analyser/nsrt/type-aliases.php index 9ad1e39918..dd7440470f 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/nsrt/type-aliases.php @@ -90,7 +90,7 @@ public function globalAlias($parameter) */ public function localAlias($parameter) { - assertType('callable(string): string|false', $parameter); + assertType('callable(string): (string|false)', $parameter); } /** @@ -98,7 +98,7 @@ public function localAlias($parameter) */ public function nestedLocalAlias($parameter) { - assertType('array', $parameter); + assertType('array', $parameter); } /** @@ -134,7 +134,7 @@ public function invalidImports($parameter1, $parameter2, $parameter3) */ public function conflictingAlias($parameter) { - assertType('*NEVER*', $parameter); + assertType('never', $parameter); } public function __get(string $name) @@ -151,7 +151,7 @@ public function testIntAlias($int) } assertType('int|string', (new Foo)->globalAliasProperty); - assertType('callable(string): string|false', (new Foo)->localAliasProperty); + assertType('callable(string): (string|false)', (new Foo)->localAliasProperty); assertType('Countable&Traversable', (new Foo)->importedAliasProperty); assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty); assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty); @@ -181,7 +181,7 @@ class UsesTrait1 /** @param Test $a */ public function doBar($a) { - assertType('array(string, int)', $a); + assertType('array{string, int}', $a); assertType(Test::class, $this->doFoo()); } diff --git a/tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php b/tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php similarity index 100% rename from tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php rename to tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php diff --git a/tests/PHPStan/Analyser/data/uksort-bug.php b/tests/PHPStan/Analyser/nsrt/uksort-bug.php similarity index 100% rename from tests/PHPStan/Analyser/data/uksort-bug.php rename to tests/PHPStan/Analyser/nsrt/uksort-bug.php diff --git a/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php new file mode 100644 index 0000000000..afda5d2229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php @@ -0,0 +1,48 @@ + $filteredParameters + */ + public function doFoo(array $filteredParameters, array $a): void + { + $otherFilteredParameters = $filteredParameters; + foreach ($a as $k => $v) { + if (rand(0, 1)) { + unset($otherFilteredParameters[$k]); + } + } + + if (count($otherFilteredParameters) > 0) { + return; + } + + assertType('array{}', $otherFilteredParameters); + assertType('array', $filteredParameters); + } + + public function doBaz(): void + { + $breakdowns = [ + 'a' => (bool) rand(0, 1), + 'b' => (string) rand(0, 1), + 'c' => rand(-1, 1), + 'd' => rand(0, 1), + ]; + + foreach ($breakdowns as $type => $bd) { + if (empty($bd)) { + unset($breakdowns[$type]); + } + } + + assertType("array{a?: bool, b?: '0'|'1', c?: int<-1, 1>, d?: int<0, 1>}", $breakdowns); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php new file mode 100644 index 0000000000..2ddf808da2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php @@ -0,0 +1,22 @@ + $commonStrings + * @param array $uppercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $uppercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $uppercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('uppercase-string', implode($ls, $uppercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php new file mode 100644 index 0000000000..7045582dc4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php @@ -0,0 +1,23 @@ += 8.0 + +namespace UppercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param uppercase-string $uppercase + */ + public function doSubstr(string $uppercase): void + { + assertType('uppercase-string', substr($uppercase, 5)); + assertType('uppercase-string', substr($uppercase, -5)); + assertType('uppercase-string', substr($uppercase, 0, 5)); + } + + /** + * @param uppercase-string $uppercase + */ + public function doMbSubstr(string $uppercase): void + { + assertType('uppercase-string', mb_substr($uppercase, 5)); + assertType('uppercase-string', mb_substr($uppercase, -5)); + assertType('uppercase-string', mb_substr($uppercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php new file mode 100644 index 0000000000..0c24268faf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -0,0 +1,29 @@ += 8.1 + +declare(strict_types=1); + +namespace ValueOfEnum; + +use function PHPStan\Testing\assertType; + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +class Foo { + /** + * @return value-of + */ + function us() + { + return Country::US; + } + + /** + * @param value-of $countryName + */ + function hello($countryName) + { + assertType("'The Netherlands'|'United States'", $countryName); + } + + function doFoo() { + assertType("'The Netherlands'|'United States'", $this->us()); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/value-of-generic.php b/tests/PHPStan/Analyser/nsrt/value-of-generic.php new file mode 100644 index 0000000000..97d966a799 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/value-of-generic.php @@ -0,0 +1,28 @@ + + */ +interface Result +{ + /** + * @return value-of|false + */ + public function getColumn(); +} + +/** + * @param Result $result + * @param Result $emptyResult + */ +function test( + Result $result, + Result $emptyResult +): void { + assertType('int|string|false', $result->getColumn()); + assertType('false', $emptyResult->getColumn()); +} diff --git a/tests/PHPStan/Analyser/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/data/var-above-declare.php b/tests/PHPStan/Analyser/nsrt/var-above-declare.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-declare.php rename to tests/PHPStan/Analyser/nsrt/var-above-declare.php diff --git a/tests/PHPStan/Analyser/data/var-above-use.php b/tests/PHPStan/Analyser/nsrt/var-above-use.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-use.php rename to tests/PHPStan/Analyser/nsrt/var-above-use.php diff --git a/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php b/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php new file mode 100644 index 0000000000..6550d139b7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php @@ -0,0 +1,25 @@ += 8.0 + +namespace VariadicParameterPHP8; + +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/version-compare-php7.php b/tests/PHPStan/Analyser/nsrt/version-compare-php7.php new file mode 100644 index 0000000000..663f7edee0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/version-compare-php7.php @@ -0,0 +1,32 @@ +' $unionValid + * @param '<'|'a' $unionBoth + * @param 'a'|'b' $unionInvalid + */ + public function fgetss( + string $string, + string $unionValid, + string $unionBoth, + string $unionInvalid, + ) : void + { + assertType('(bool|null)', \version_compare($string, $string, $string)); + + assertType('false', \version_compare('Foo','Bar','<')); + assertType('(bool|null)', \version_compare('Foo','Bar', $string)); + assertType('false', \version_compare('Foo','Bar', $unionValid)); + assertType('false|null', \version_compare('Foo','Bar', $unionBoth)); + assertType('null', \version_compare('Foo','Bar', $unionInvalid)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/version-compare-php8.php b/tests/PHPStan/Analyser/nsrt/version-compare-php8.php new file mode 100644 index 0000000000..d539e53570 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/version-compare-php8.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types=1); + +namespace VersionComparePHP8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param string $string + * @param '<'|'>' $unionValid + * @param '<'|'a' $unionBoth + * @param 'a'|'b' $unionInvalid + */ + public function fgetss( + string $string, + string $unionValid, + string $unionBoth, + string $unionInvalid, + ) : void + { + assertType('bool', \version_compare($string, $string, $string)); + + assertType('false', \version_compare('Foo','Bar','<')); + assertType('bool', \version_compare('Foo','Bar', $string)); + assertType('false', \version_compare('Foo','Bar', $unionValid)); + assertType('false', \version_compare('Foo','Bar', $unionBoth)); + assertType('*NEVER*', \version_compare('Foo','Bar', $unionInvalid)); + } +} 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 ee7dd1f215..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php +++ /dev/null @@ -1,184 +0,0 @@ -deleteCache(); - - if ($this->originalTraitOneContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitOne.php', $this->originalTraitOneContents); - } - - if ($this->originalTraitTwoContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitTwo.php', $this->originalTraitTwoContents); - } - } - - public function dataCachingIssue(): array - { - return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - false, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - true, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - ]; - } - - /** - * @dataProvider dataCachingIssue - * @param bool $changeOne - * @param bool $changeTwo - * @param string[] $expectedErrors - */ - public function testCachingIssue( - bool $changeOne, - bool $changeTwo, - array $expectedErrors - ): void - { - $this->deleteCache(); - [$statusCode, $errors] = $this->runPhpStan(); - $this->assertSame([], $errors); - $this->assertSame(0, $statusCode); - - if ($changeOne) { - $this->originalTraitOneContents = $this->changeTrait(__DIR__ . '/data/TraitOne.php'); - } - if ($changeTwo) { - $this->originalTraitTwoContents = $this->changeTrait(__DIR__ . '/data/TraitTwo.php'); - } - - $fileHelper = new FileHelper(__DIR__); - - $errorPath = $fileHelper->normalizePath(__DIR__ . '/data/TestClassUsingTrait.php'); - [$statusCode, $errors] = $this->runPhpStan(); - - if (count($expectedErrors) === 0) { - $this->assertSame(0, $statusCode); - $this->assertArrayNotHasKey($errorPath, $errors); - return; - } - - $this->assertSame(1, $statusCode); - $this->assertArrayHasKey($errorPath, $errors); - $this->assertSame(count($expectedErrors), $errors[$errorPath]['errors']); - - foreach ($errors[$errorPath]['messages'] as $i => $error) { - $this->assertSame($expectedErrors[$i], $error['message']); - } - } - - /** - * @return array{int, mixed[]} - */ - private function runPhpStan(): array - { - $phpstanBinPath = __DIR__ . '/../../../../bin/phpstan'; - exec(sprintf('%s %s clear-result-cache --configuration %s', escapeshellarg(PHP_BINARY), $phpstanBinPath, escapeshellarg(__DIR__ . '/phpstan.neon')), $clearResultCacheOutputLines, $clearResultCacheExitCode); - if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); - } - - exec( - sprintf( - '%s %s analyse --no-progress --level 8 --configuration %s --error-format json %s', - escapeshellarg(PHP_BINARY), - $phpstanBinPath, - escapeshellarg(__DIR__ . '/phpstan.neon'), - escapeshellarg(__DIR__ . '/data') - ), - $output, - $statusCode - ); - $stringOutput = implode("\n", $output); - $json = \Nette\Utils\Json::decode($stringOutput, \Nette\Utils\Json::FORCE_ARRAY); - - return [$statusCode, $json['files']]; - } - - private function deleteCache(): void - { - $dir = __DIR__ . '/tmp/cache'; - if (!file_exists($dir)) { - return; - } - - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator(__DIR__ . '/tmp/cache', RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileinfo) { - if ($fileinfo->isDir()) { - rmdir($fileinfo->getRealPath()); - continue; - } - - unlink($fileinfo->getRealPath()); - } - } - - private function changeTrait(string $traitPath): string - { - $originalTraitContents = FileReader::read($traitPath); - $traitContents = str_replace('use stdClass as Foo;', 'use Exception as Foo;', $originalTraitContents); - $result = file_put_contents($traitPath, $traitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - - return $originalTraitContents; - } - - private function revertTrait(string $traitPath, string $originalTraitContents): void - { - $result = file_put_contents($traitPath, $originalTraitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - } - -} diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore b/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore deleted file mode 100644 index d6b7ef32c8..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/PHPStan/Analyser/unknown-mixed-type.neon b/tests/PHPStan/Analyser/unknown-mixed-type.neon new file mode 100644 index 0000000000..a9d8e60640 --- /dev/null +++ b/tests/PHPStan/Analyser/unknown-mixed-type.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 70400 diff --git a/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon new file mode 100644 index 0000000000..12225ce6e6 --- /dev/null +++ b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon @@ -0,0 +1,2 @@ +parameters: + usePathConstantsAsConstantString: true diff --git a/tests/PHPStan/Broker/BrokerTest.php b/tests/PHPStan/Broker/BrokerTest.php deleted file mode 100644 index 86d2ad406c..0000000000 --- a/tests/PHPStan/Broker/BrokerTest.php +++ /dev/null @@ -1,89 +0,0 @@ -getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - - $workingDirectory = __DIR__; - $relativePathHelper = new SimpleRelativePathHelper($workingDirectory); - $fileHelper = new FileHelper($workingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, $relativePathHelper); - - $classReflectionExtensionRegistryProvider = new DirectClassReflectionExtensionRegistryProvider([], []); - - $setterReflectionProviderProvider = new SetterReflectionProviderProvider(); - $reflectionProvider = new RuntimeReflectionProvider( - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $this->createMock(FunctionReflectionFactory::class), - new FileTypeMapper($setterReflectionProviderProvider, $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), $anonymousClassNameHelper), - self::getContainer()->getByType(PhpDocInheritanceResolver::class), - self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(StubPhpDocProvider::class), - self::getContainer()->getByType(PhpStormStubsSourceStubber::class) - ); - $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); - $this->broker = new Broker( - $reflectionProvider, - [] - ); - $classReflectionExtensionRegistryProvider->setBroker($this->broker); - } - - public function testClassNotFound(): void - { - $this->expectException(\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('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/Build/AttributeNamedArgumentsRuleTest.php b/tests/PHPStan/Build/AttributeNamedArgumentsRuleTest.php new file mode 100644 index 0000000000..2e3c99ad9e --- /dev/null +++ b/tests/PHPStan/Build/AttributeNamedArgumentsRuleTest.php @@ -0,0 +1,34 @@ + + */ +class AttributeNamedArgumentsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AttributeNamedArgumentsRule(self::createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/attribute-arguments.php'], [ + [ + 'Attribute PHPStan\DependencyInjection\AutowiredService is not using named arguments.', + 13, + ], + ]); + } + + public function testFix(): void + { + $this->fix(__DIR__ . '/data/attribute-arguments.php', __DIR__ . '/data/attribute-arguments.php.fixed'); + } + +} diff --git a/tests/PHPStan/Build/FinalClassRuleTest.php b/tests/PHPStan/Build/FinalClassRuleTest.php new file mode 100644 index 0000000000..4a09327a38 --- /dev/null +++ b/tests/PHPStan/Build/FinalClassRuleTest.php @@ -0,0 +1,35 @@ + + */ +class FinalClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalClassRule(self::getContainer()->getByType(FileHelper::class), false); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/final-class-rule.php'], [ + [ + 'Class FinalClassRule\Baz must be abstract or final.', + 29, + ], + ]); + } + + public function testFix(): void + { + $this->fix(__DIR__ . '/data/final-class-rule.php', __DIR__ . '/data/final-class-rule.php.fixed'); + } + +} diff --git a/tests/PHPStan/Build/MemoizationPropertyRuleTest.php b/tests/PHPStan/Build/MemoizationPropertyRuleTest.php new file mode 100644 index 0000000000..8eb191d0bd --- /dev/null +++ b/tests/PHPStan/Build/MemoizationPropertyRuleTest.php @@ -0,0 +1,53 @@ + + */ +final class MemoizationPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MemoizationPropertyRule(self::getContainer()->getByType(FileHelper::class), false); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/memoization-property.php'], [ + [ + 'This initializing if statement can be replaced with null coalescing assignment operator (??=).', + 13, + ], + [ + 'This initializing if statement can be replaced with null coalescing assignment operator (??=).', + 22, + ], + [ + 'This initializing if statement can be replaced with null coalescing assignment operator (??=).', + 55, + ], + [ + 'This initializing if statement can be replaced with null coalescing assignment operator (??=).', + 85, + ], + [ + 'This initializing if statement can be replaced with null coalescing assignment operator (??=).', + 96, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testFix(): void + { + $this->fix(__DIR__ . '/data/memoization-property.php', __DIR__ . '/data/memoization-property.php.fixed'); + } + +} diff --git a/tests/PHPStan/Build/NamedArgumentsRuleTest.php b/tests/PHPStan/Build/NamedArgumentsRuleTest.php new file mode 100644 index 0000000000..f0178e8c1f --- /dev/null +++ b/tests/PHPStan/Build/NamedArgumentsRuleTest.php @@ -0,0 +1,93 @@ + + */ +class NamedArgumentsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NamedArgumentsRule(self::createReflectionProvider(), new PhpVersion(PHP_VERSION_ID)); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/named-arguments.php'], [ + [ + 'You\'re passing a non-default value Exception to parameter $previous but previous argument is passing default value to its parameter ($code). You can skip it and use named argument for $previous instead.', + 14, + ], + [ + 'Named argument $code can be omitted, type 0 is the same as the default value.', + 16, + ], + [ + 'You\'re passing a non-default value Exception to parameter $previous but previous arguments are passing default values to their parameters ($message, $code). You can skip them and use named argument for $previous instead.', + 20, + ], + [ + 'You\'re passing a non-default value 3 to parameter $yetAnother but previous argument is passing default value to its parameter ($another). You can skip it and use named argument for $yetAnother instead.', + 41, + ], + [ + 'Named argument $priority can be omitted, type 1 is the same as the default value.', + 59, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testNoFix(): void + { + $this->fix( + __DIR__ . '/data/named-arguments-no-errors.php', + __DIR__ . '/data/named-arguments-no-errors.php', + ); + } + + #[RequiresPhp('>= 8.0')] + public function testFix(): void + { + $this->fix( + __DIR__ . '/data/named-arguments.php', + __DIR__ . '/data/named-arguments.php.fixed', + ); + } + + #[RequiresPhp('>= 8.0')] + public function testFixFileWithMatch(): void + { + $this->fix( + __DIR__ . '/data/named-arguments-match.php', + __DIR__ . '/data/named-arguments-match.php.fixed', + ); + } + + #[RequiresPhp('>= 8.1')] + public function testNewInInitializer(): void + { + $this->analyse([__DIR__ . '/data/named-arguments-new.php'], [ + [ + 'You\'re passing a non-default value \'bar\' to parameter $d but previous argument is passing default value to its parameter ($c). You can skip it and use named argument for $d instead.', + 24, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testFixNewInInitializer(): void + { + $this->fix(__DIR__ . '/data/named-arguments-new.php', __DIR__ . '/data/named-arguments-new.php.fixed'); + } + +} diff --git a/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php b/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php new file mode 100644 index 0000000000..06caa79fa4 --- /dev/null +++ b/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php @@ -0,0 +1,49 @@ + + */ +final class OrChainIdenticalComparisonToInArrayRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OrChainIdenticalComparisonToInArrayRule(new ExprPrinter(new Printer()), self::getContainer()->getByType(FileHelper::class), false); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/or-chain-identical-comparison.php'], [ + [ + 'This chain of identical comparisons can be simplified using in_array().', + 7, + ], + [ + 'This chain of identical comparisons can be simplified using in_array().', + 11, + ], + [ + 'This chain of identical comparisons can be simplified using in_array().', + 15, + ], + [ + 'This chain of identical comparisons can be simplified using in_array().', + 17, + ], + ]); + } + + public function testFix(): void + { + $this->fix(__DIR__ . '/data/or-chain-identical-comparison.php', __DIR__ . '/data/or-chain-identical-comparison.php.fixed'); + } + +} diff --git a/tests/PHPStan/Build/data/attribute-arguments.php b/tests/PHPStan/Build/data/attribute-arguments.php new file mode 100644 index 0000000000..12ca9cbcea --- /dev/null +++ b/tests/PHPStan/Build/data/attribute-arguments.php @@ -0,0 +1,17 @@ += 8.0 + +namespace MemoizationProperty; + +final class A +{ + private ?string $foo = null; + private ?string $bar = null; + private string|false $buz = false; + + public function getFoo() + { + if ($this->foo === null) { + $this->foo = random_bytes(1); + } + + return $this->foo; + } + + public function getBar() + { + if ($this->bar === null) { + $this->bar = random_bytes(1); + } + + return $this->bar; + } + + /** Not applicable because it has an else clause in the if. */ + public function getBarElse() + { + if ($this->bar === null) { + $this->bar = random_bytes(1); + } else { + // no-op + } + + return $this->bar; + } + + /** Not applicable because it has an elseif clause in the if. */ + public function getBarElseIf() + { + if ($this->bar === null) { + $this->bar = random_bytes(1); + } elseif (false) { + // no-op + } + + return $this->bar; + } + + public function getBarReceiveParam(int $length) + { + if ($this->bar === null) { + $this->bar = random_bytes($length); + } + + return $this->bar; + } + + /** Not applicable because the body of if is not just an assignment. */ + public function getBarComplex() + { + if ($this->bar === null) { + $rand = random_bytes(1); + $this->bar = $rand; + } + + return $this->bar; + } + + /** Not applicable because it is comparing a property with a non-null value. */ + public function getBuz() + { + if ($this->buz === false) { + $this->buz = random_bytes(1); + } + + return $this->buz; + } + + public function printFoo(): void + { + if ($this->foo === null) { + $this->foo = random_bytes(1); + } + + echo $this->foo; + } + + private static ?self $singleton = null; + + public static function singleton(): self + { + if (self::$singleton === null) { + self::$singleton = new self(); + } + + return self::$singleton; + } + + /** Not applicable because property names are not matched. */ + public static function singletonBadProperty(): self + { + if (self::$singleton === null) { + self::$singletom = new self(); + } + + return self::$singleton; + } + +} diff --git a/tests/PHPStan/Build/data/memoization-property.php.fixed b/tests/PHPStan/Build/data/memoization-property.php.fixed new file mode 100644 index 0000000000..73a40f8ac7 --- /dev/null +++ b/tests/PHPStan/Build/data/memoization-property.php.fixed @@ -0,0 +1,103 @@ += 8.0 + +namespace MemoizationProperty; + +final class A +{ + private ?string $foo = null; + private ?string $bar = null; + private string|false $buz = false; + + public function getFoo() + { + $this->foo ??= random_bytes(1); + + return $this->foo; + } + + public function getBar() + { + $this->bar ??= random_bytes(1); + + return $this->bar; + } + + /** Not applicable because it has an else clause in the if. */ + public function getBarElse() + { + if ($this->bar === null) { + $this->bar = random_bytes(1); + } else { + // no-op + } + + return $this->bar; + } + + /** Not applicable because it has an elseif clause in the if. */ + public function getBarElseIf() + { + if ($this->bar === null) { + $this->bar = random_bytes(1); + } elseif (false) { + // no-op + } + + return $this->bar; + } + + public function getBarReceiveParam(int $length) + { + $this->bar ??= random_bytes($length); + + return $this->bar; + } + + /** Not applicable because the body of if is not just an assignment. */ + public function getBarComplex() + { + if ($this->bar === null) { + $rand = random_bytes(1); + $this->bar = $rand; + } + + return $this->bar; + } + + /** Not applicable because it is comparing a property with a non-null value. */ + public function getBuz() + { + if ($this->buz === false) { + $this->buz = random_bytes(1); + } + + return $this->buz; + } + + public function printFoo(): void + { + $this->foo ??= random_bytes(1); + + echo $this->foo; + } + + private static ?self $singleton = null; + + public static function singleton(): self + { + self::$singleton ??= new self(); + + return self::$singleton; + } + + /** Not applicable because property names are not matched. */ + public static function singletonBadProperty(): self + { + if (self::$singleton === null) { + self::$singletom = new self(); + } + + return self::$singleton; + } + +} diff --git a/tests/PHPStan/Build/data/named-arguments-match.php b/tests/PHPStan/Build/data/named-arguments-match.php new file mode 100644 index 0000000000..f53c87832c --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments-match.php @@ -0,0 +1,16 @@ += 8.0 + +namespace NamedArgumentsMatchRule; + +use Exception; + +function (bool $a, bool $b): void { + foreach ([1, 2, 3] as $v) { + match (true) { + $a => 1, + $b => 2, + default => 3, + }; + new Exception('foo', 0, new Exception('prev')); + } +}; diff --git a/tests/PHPStan/Build/data/named-arguments-match.php.fixed b/tests/PHPStan/Build/data/named-arguments-match.php.fixed new file mode 100644 index 0000000000..9cec06b484 --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments-match.php.fixed @@ -0,0 +1,16 @@ += 8.0 + +namespace NamedArgumentsMatchRule; + +use Exception; + +function (bool $a, bool $b): void { + foreach ([1, 2, 3] as $v) { + match (true) { + $a => 1, + $b => 2, + default => 3, + }; + new Exception('foo', previous: new Exception('prev')); + } +}; diff --git a/tests/PHPStan/Build/data/named-arguments-new.php b/tests/PHPStan/Build/data/named-arguments-new.php new file mode 100644 index 0000000000..61f1c151d2 --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments-new.php @@ -0,0 +1,25 @@ += 8.1 + +namespace NamedArgumentsRuleNew; + +class Bar +{ + +} + +class Foo +{ + + public static function doFoo(int $a, Bar $bar = new Bar(), string $c = 'bar', string $d = 'baz'): void + { + + } + +} + +function (): void { + Foo::doFoo(1, new Bar(), 'bar'); + Foo::doFoo(1, new Bar(), 'baz'); + + Foo::doFoo(1, new Bar(), 'bar', 'bar'); +}; diff --git a/tests/PHPStan/Build/data/named-arguments-new.php.fixed b/tests/PHPStan/Build/data/named-arguments-new.php.fixed new file mode 100644 index 0000000000..2cf2934112 --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments-new.php.fixed @@ -0,0 +1,25 @@ += 8.1 + +namespace NamedArgumentsRuleNew; + +class Bar +{ + +} + +class Foo +{ + + public static function doFoo(int $a, Bar $bar = new Bar(), string $c = 'bar', string $d = 'baz'): void + { + + } + +} + +function (): void { + Foo::doFoo(1, new Bar(), 'bar'); + Foo::doFoo(1, new Bar(), 'baz'); + + Foo::doFoo(1, new Bar(), d: 'bar'); +}; diff --git a/tests/PHPStan/Build/data/named-arguments-no-errors.php b/tests/PHPStan/Build/data/named-arguments-no-errors.php new file mode 100644 index 0000000000..d9d57f9d0b --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments-no-errors.php @@ -0,0 +1,10 @@ += 8.0 + +namespace NamedArgumentRuleNoErrors; + +use Exception; + +function (): void { + new Exception('foo', 0); + new Exception('foo', 0, null); +}; diff --git a/tests/PHPStan/Build/data/named-arguments.php b/tests/PHPStan/Build/data/named-arguments.php new file mode 100644 index 0000000000..d8db59f36f --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments.php @@ -0,0 +1,63 @@ += 8.0 + +namespace NamedArgumentRule; + +use Exception; + +class Foo +{ + + public function doFoo(): void + { + new Exception('foo', 0); + new Exception('foo', 0, null); + new Exception('foo', 0, new Exception('previous')); + new Exception('foo', previous: new Exception('previous')); + new Exception('foo', code: 0, previous: new Exception('previous')); + new Exception('foo', code: 1, previous: new Exception('previous')); + new Exception('foo', 1, new Exception('previous')); + new Exception('foo', 1); + new Exception('', 0, new Exception('previous')); + } + +} + +function (): void { + $output = null; + exec('exec', $output, $exitCode); +}; + +class Bar +{ + + public static function doFoo($a, &$byRef = null, int $another = 1, int $yetAnother = 2): void + { + + } + + public function doBar(): void + { + $byRef = null; + self::doFoo('a', $byRef, 1, 3); + } + +} + +class Baz +{ + + public const HIGH = 1; + public const MEDIUM = 2; + + public static function send(Bar $message, ?string $queueName = null, ?Bar $mode = null, int $priority = self::HIGH) + { + + } + + public function doFoo(Bar $message): void + { + self::send($message, 'queue', priority: self::HIGH); + self::send($message, 'queue', priority: self::MEDIUM); + } + +} diff --git a/tests/PHPStan/Build/data/named-arguments.php.fixed b/tests/PHPStan/Build/data/named-arguments.php.fixed new file mode 100644 index 0000000000..cf1838943c --- /dev/null +++ b/tests/PHPStan/Build/data/named-arguments.php.fixed @@ -0,0 +1,63 @@ += 8.0 + +namespace NamedArgumentRule; + +use Exception; + +class Foo +{ + + public function doFoo(): void + { + new Exception('foo', 0); + new Exception('foo', 0, null); + new Exception('foo', previous: new Exception('previous')); + new Exception('foo', previous: new Exception('previous')); + new Exception('foo', previous: new Exception('previous')); + new Exception('foo', code: 1, previous: new Exception('previous')); + new Exception('foo', 1, new Exception('previous')); + new Exception('foo', 1); + new Exception(previous: new Exception('previous')); + } + +} + +function (): void { + $output = null; + exec('exec', $output, $exitCode); +}; + +class Bar +{ + + public static function doFoo($a, &$byRef = null, int $another = 1, int $yetAnother = 2): void + { + + } + + public function doBar(): void + { + $byRef = null; + self::doFoo('a', $byRef, yetAnother: 3); + } + +} + +class Baz +{ + + public const HIGH = 1; + public const MEDIUM = 2; + + public static function send(Bar $message, ?string $queueName = null, ?Bar $mode = null, int $priority = self::HIGH) + { + + } + + public function doFoo(Bar $message): void + { + self::send($message, 'queue'); + self::send($message, 'queue', priority: self::MEDIUM); + } + +} diff --git a/tests/PHPStan/Build/data/or-chain-identical-comparison.php b/tests/PHPStan/Build/data/or-chain-identical-comparison.php new file mode 100644 index 0000000000..ddce971508 --- /dev/null +++ b/tests/PHPStan/Build/data/or-chain-identical-comparison.php @@ -0,0 +1,31 @@ + + */ +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 c4605b5bcb..b13cb49ef9 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -3,15 +3,26 @@ namespace PHPStan\Command; use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter; +use PHPStan\Command\ErrorFormatter\GithubErrorFormatter; use PHPStan\Command\ErrorFormatter\TableErrorFormatter; +use PHPStan\Command\ErrorFormatter\TeamcityErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; +use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Style\SymfonyStyle; +use function fopen; +use function rewind; +use function sprintf; +use function stream_get_contents; +use const DIRECTORY_SEPARATOR; -class AnalyseApplicationIntegrationTest extends \PHPStan\Testing\PHPStanTestCase +class AnalyseApplicationIntegrationTest extends PHPStanTestCase { public function testExecuteOnAFile(): void @@ -26,7 +37,7 @@ public function testExecuteOnANonExistentPath(): void $output = $this->runPath($path, 1); $this->assertStringContainsString(sprintf( 'File %s does not exist.', - $path + $path, ), $output); } @@ -39,49 +50,50 @@ public function testExecuteOnAFileWithErrors(): void private function runPath(string $path, int $expectedStatusCode): string { - if (PHP_VERSION_ID >= 80000 && DIRECTORY_SEPARATOR === '\\') { - $this->markTestSkipped('Skipped because of https://github.com/symfony/symfony/issues/37508'); - } self::getContainer()->getByType(ResultCacheClearer::class)->clear(); $analyserApplication = self::getContainer()->getByType(AnalyseApplication::class); $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); $symfonyOutput = new SymfonyOutput( $output, - new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)) + new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)), ); - $memoryLimitFile = self::getContainer()->getParameter('memoryLimitFile'); - $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), __DIR__, [], DIRECTORY_SEPARATOR); - $errorFormatter = new TableErrorFormatter($relativePathHelper, false); + $errorFormatter = new TableErrorFormatter( + $relativePathHelper, + 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, null, - $this->createMock(InputInterface::class) + null, + $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(); - } + $this->assertSame($expectedStatusCode, $statusCode, $contents); return $contents; } diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 5fddff391f..f2c4f4d73a 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -2,32 +2,37 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; 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\PHPStanTestCase +#[Group('exec')] +class AnalyseCommandTest extends PHPStanTestCase { - /** - * @param string $dir - * @param string $file - * @dataProvider autoDiscoveryPathsProvider - */ + #[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 +49,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,37 +76,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-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-dist', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-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 { - if (PHP_VERSION_ID >= 80000 && DIRECTORY_SEPARATOR === '\\') { - $this->markTestSkipped('Skipped because of https://github.com/symfony/symfony/issues/37508'); - } - $commandTester = new CommandTester(new AnalyseCommand([])); + $commandTester = 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 7be232359a..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -39,10 +39,14 @@ public function testErrorsAreSortedByFileNameAndLine(): void [], [], [], + [], false, null, - true - ))->getFileSpecificErrors() + true, + 0, + false, + [], + ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index 191b70fbfc..5ecf380ddb 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -2,19 +2,24 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; 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 - */ +#[Group('exec')] class CommandHelperTest extends TestCase { - public function dataBegin(): array + public static function dataBegin(): array { return [ [ @@ -89,26 +94,21 @@ 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 */ + #[DataProvider('dataBegin')] public function testBegin( string $input, string $expectedOutput, ?string $projectConfigFile, ?string $level, array $expectedParameters, - bool $expectException + bool $expectException, ): void { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); @@ -119,24 +119,23 @@ public function testBegin( [__DIR__], null, null, - null, [], $projectConfigFile, null, $level, false, - true + false, + null, + null, + 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(); - } $this->fail($contents); } } @@ -144,9 +143,6 @@ public function testBegin( rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); - if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); - } $this->assertStringContainsString($expectedOutput, $contents); if (isset($result)) { @@ -160,7 +156,7 @@ public function testBegin( } } - public function dataParameters(): array + public static function dataParameters(): array { return [ [ @@ -169,7 +165,8 @@ public function dataParameters(): array 'bootstrapFiles' => [ realpath(__DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'), realpath(__DIR__ . '/../../../stubs/runtime/ReflectionAttribute.php'), - realpath(__DIR__ . '/../../../stubs/runtime/Attribute.php'), + realpath(__DIR__ . '/../../../stubs/runtime/Attribute85.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionIntersectionType.php'), __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', ], 'scanFiles' => [ @@ -185,7 +182,6 @@ public function dataParameters(): array 'paths' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', ], - 'memoryLimitFile' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . '.memory_limit', 'excludePaths' => [ 'analyseAndScan' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', @@ -204,6 +200,7 @@ public function dataParameters(): array __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'up.php', ], + 'reportUnmatchedIgnoredErrors' => false, 'ignoreErrors' => [ [ 'message' => '#aaa#', @@ -235,12 +232,12 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/full.neon', [ 'excludePaths' => [ - 'analyse' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', - ], 'analyseAndScan' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], + 'analyse' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], ], ], ], @@ -248,13 +245,13 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/including.neon', [ 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', + ], 'analyse' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], - 'analyseAndScan' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', - ], ], ], ], @@ -288,14 +285,13 @@ public function dataParameters(): array } /** - * @dataProvider dataParameters - * @param string $configFile * @param array $expectedParameters - * @throws \PHPStan\Command\InceptionNotSuccessfulException + * @throws InceptionNotSuccessfulException */ + #[DataProvider('dataParameters')] public function testResolveParameters( string $configFile, - array $expectedParameters + array $expectedParameters, ): void { $result = CommandHelper::begin( @@ -304,13 +300,15 @@ public function testResolveParameters( [__DIR__], null, null, - null, [], $configFile, null, '0', false, - true + false, + null, + null, + 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 e74ec77c8a..4cab9de19b 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php @@ -3,13 +3,20 @@ namespace PHPStan\Command\ErrorFormatter; use Nette\Utils\Json; +use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; +use function array_sum; use function chdir; +use function escapeshellarg; +use function exec; use function getcwd; +use function implode; +use function sprintf; +use function unlink; +use const PHP_BINARY; -/** - * @group exec - */ +#[Group('exec')] class BaselineNeonErrorFormatterIntegrationTest extends TestCase { @@ -53,17 +60,17 @@ private function runPhpStan( string $analysedPath, ?string $configFile, string $errorFormatter = 'json', - ?string $baselineFile = null + ?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%s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $errorFormatter, $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : '', escapeshellarg($analysedPath), $baselineFile !== null ? ' --generate-baseline ' . escapeshellarg($baselineFile) : ''), $outputLines); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 49890696cb..1f9f4bfd27 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -2,25 +2,41 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +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 { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', 0, 0, 0, + false, [], ]; @@ -29,6 +45,7 @@ public function dataFormatterOutputProvider(): iterable 1, 1, 0, + false, [ [ 'message' => '#^Foo$#', @@ -43,6 +60,7 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 0, + false, [ [ 'message' => "#^Bar\nBar2$#", @@ -60,7 +78,7 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -72,6 +90,7 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 2, + false, [ [ 'message' => "#^Bar\nBar2$#", @@ -89,7 +108,37 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', + 'count' => 1, + 'path' => 'foo.php', + ], + ], + ]; + + yield [ + 'Multiple file, multiple generic errors (raw messages)', + 1, + 4, + 2, + true, + [ + [ + 'rawMessage' => "Bar\nBar2", + 'count' => 1, + 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', + ], + [ + 'rawMessage' => 'Foo', + 'count' => 1, + 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', + ], + [ + 'rawMessage' => "Bar\nBar2", + 'count' => 1, + 'path' => 'foo.php', + ], + [ + 'rawMessage' => 'Foo', 'count' => 1, 'path' => 'foo.php', ], @@ -98,49 +147,50 @@ public function dataFormatterOutputProvider(): iterable } /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors * @param mixed[] $expected */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - array $expected + bool $useRawMessage, + array $expected, ): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), $useRawMessage); $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)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); $result = new AnalysisResult( [new Error('Escape Regex with file # ~ \' ()', 'Testfile')], ['Escape Regex without file # ~ <> \' ()'], [], [], + [], false, null, - true + true, + 0, + false, + [], ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), + '', ); self::assertSame( @@ -155,51 +205,92 @@ public function testFormatErrorMessagesRegexEscape(): void ], ], ], - ], Neon::BLOCK) + ], Neon::BLOCK), ), - trim($this->getOutputContent()) + trim($this->getOutputContent()), ); } - public function testEscapeDiNeon(): void + /** + * @return iterable}> + */ + public static function dataEscapeDiNeon(): iterable + { + yield [ + new Error('Test %value%', 'Testfile'), + false, + [ + 'message' => '#^Test %%value%%$#', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + + yield [ + new Error('Test %value%', 'Testfile'), + true, + [ + 'rawMessage' => 'Test %%value%%', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + + yield [ + new Error('@Foo', 'Testfile'), + true, + [ + 'rawMessage' => '@@Foo', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + } + + /** + * @param array $expected + */ + #[DataProvider('dataEscapeDiNeon')] + public function testEscapeDiNeon(Error $error, bool $useRawMessage, array $expected): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), $useRawMessage); $result = new AnalysisResult( - [new Error('Test %value%', 'Testfile')], + [$error], + [], [], [], [], false, null, - true + true, + 0, + false, + [], ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), + '', ); self::assertSame( trim( Neon::encode([ 'parameters' => [ 'ignoreErrors' => [ - [ - 'message' => '#^Test %%value%%$#', - 'count' => 1, - 'path' => 'Testfile', - ], + $expected, ], ], - ], Neon::BLOCK) + ], Neon::BLOCK), ), - trim($this->getOutputContent()) + trim($this->getOutputContent()), ); } /** - * @return \Generator}, void, void> + * @return Generator}, void, void> */ - public function outputOrderingProvider(): \Generator + public static function outputOrderingProvider(): Generator { $errors = [ new Error('Error #2', 'TestfileA', 1), @@ -220,25 +311,30 @@ public function outputOrderingProvider(): \Generator } /** - * @dataProvider outputOrderingProvider * @param list $errors */ + #[DataProvider('outputOrderingProvider')] public function testOutputOrdering(array $errors): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); $result = new AnalysisResult( $errors, [], [], [], + [], false, null, - true + true, + 0, + false, + [], ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), + '', ); self::assertSame( trim(Neon::encode([ @@ -282,8 +378,318 @@ public function testOutputOrdering(array $errors): void ], ], ], Neon::BLOCK)), - $f = trim($this->getOutputContent()) + $f = trim($this->getOutputContent()), ); } + /** + * @return Generator}> + */ + public static 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, + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('endOfFileNewlinesProvider')] + public function testEndOfFileNewlines( + array $errors, + string $existingBaselineContent, + int $expectedNewlinesCount, + ): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); + $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, decorated: 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 ($expectedNewlinesCount > 0) { + Assert::assertSame(str_repeat("\n", $expectedNewlinesCount), substr($content, -$expectedNewlinesCount)); + } + Assert::assertNotSame("\n", substr($content, -($expectedNewlinesCount + 1), 1)); + } + + public static 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'), + ], + false, + [ + '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'), + ], + false, + [ + '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', + ], + ], + ], + ], + ]; + + 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'), + ], + true, + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'rawMessage' => 'Foo', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'rawMessage' => 'Foo with identifier', + 'identifier' => 'argument.type', + 'count' => 2, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; + } + + /** + * @param list $errors + * @param mixed[] $expectedOutput + */ + #[DataProvider('dataFormatErrorsWithIdentifiers')] + public function testFormatErrorsWithIdentifiers(array $errors, bool $useRawMessage, array $expectedOutput): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(__DIR__), $useRawMessage); + $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..2f8d95428d --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -0,0 +1,244 @@ + '#^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'), + ], + false, + " '#^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'), + ], + false, + " '#^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]]; +", + ]; + + yield [ + [ + new Error( + 'Foo A\\B\\C|null', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo A\\B\\C|null', + __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'), + ], + true, + " 'Foo A\\\\B\\\\C|null', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with another message', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with same message, different identifier', + 'identifier' => 'argument.byRef', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with same message, different identifier', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataFormatErrors')] + public function testFormatErrors(array $errors, bool $useRawMessage, string $expectedOutput): void + { + $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__), $useRawMessage); + $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 ab868c23a8..48e906017c 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -6,11 +6,13 @@ use PHPStan\Command\AnalysisResult; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class CheckstyleErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', @@ -63,7 +65,7 @@ public function dataFormatterOutputProvider(): iterable - + @@ -79,7 +81,7 @@ public function dataFormatterOutputProvider(): iterable - + ', @@ -97,40 +99,32 @@ public function dataFormatterOutputProvider(): iterable - + - + ', ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected - */ + #[DataProvider('dataFormatterOutputProvider')] 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(); @@ -145,18 +139,21 @@ public function testTraitPath(): void 'Foo', __DIR__ . '/FooTrait.php (in context of class Foo)', 5, - true, - __DIR__ . '/Foo.php', - __DIR__ . '/FooTrait.php' + filePath: __DIR__ . '/Foo.php', + traitFilePath: __DIR__ . '/FooTrait.php', ); $formatter->formatErrors(new AnalysisResult( [$error], [], [], [], + [], false, null, - true + true, + 0, + false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' @@ -165,4 +162,33 @@ public function testTraitPath(): void ', $this->getOutputContent()); } + public function testIdentifier(): void + { + $formatter = new CheckstyleErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $error = (new Error( + 'Foo', + __DIR__ . '/FooTrait.php', + 5, + filePath: __DIR__ . '/Foo.php', + ))->withIdentifier('argument.type'); + $formatter->formatErrors(new AnalysisResult( + [$error], + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), $this->getOutput()); + $this->assertXmlStringEqualsXmlString(' + + + +', $this->getOutputContent()); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php index 1b52ff5515..b349e962d4 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -5,21 +5,20 @@ use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class GithubErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', 0, 0, 0, - ' - [OK] No errors - -', + '', ]; yield [ @@ -27,15 +26,7 @@ public function dataFormatterOutputProvider(): iterable 1, 1, 0, - ' ------ ----------------------------------------------------------------- - Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- - 4 Foo - ------ ----------------------------------------------------------------- - - [ERROR] Found 1 error - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo ', ]; @@ -44,15 +35,7 @@ public function dataFormatterOutputProvider(): iterable 1, 0, 1, - ' -- --------------------- - Error - -- --------------------- - first generic error - -- --------------------- - - [ERROR] Found 1 error - -::error ::first generic error + '::error ::first generic error ', ]; @@ -61,27 +44,9 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 0, - ' ------ ----------------------------------------------------------------- - Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- - 2 Bar - Bar2 - 4 Foo - ------ ----------------------------------------------------------------- - - ------ --------- - Line foo.php - ------ --------- - 1 Foo - 5 Bar - Bar2 - ------ --------- - - [ERROR] Found 4 errors - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ', ]; @@ -91,17 +56,8 @@ public function dataFormatterOutputProvider(): iterable 1, 0, 2, - ' -- ---------------------- - Error - -- ---------------------- - first generic error - second generic error - -- ---------------------- - - [ERROR] Found 2 errors - -::error ::first generic error -::error ::second generic error + '::error ::first generic error +::error ::second generic ', ]; @@ -110,70 +66,48 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 2, - ' ------ ----------------------------------------------------------------- - Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- - 2 Bar - Bar2 - 4 Foo - ------ ----------------------------------------------------------------- - - ------ --------- - Line foo.php - ------ --------- - 1 Foo - 5 Bar - Bar2 - ------ --------- - - -- ---------------------- - Error - -- ---------------------- - first generic error - second generic error - -- ---------------------- - - [ERROR] Found 6 errors - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ::error ::first generic error -::error ::second generic error +::error ::second generic +', + ]; + + yield [ + 'One file, with @ tags', + 1, + [6, 1], + 0, + '::error file=bar.php,line=5,col=0::Error with `@param` or `@phpstan-param` and class@anonymous in the message. ', ]; } /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected + * @param array{int, int}|int $numFileErrors */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); $formatter = new GithubErrorFormatter( $relativePathHelper, - new TableErrorFormatter($relativePathHelper, false) ); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertSame($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 554e0a81c0..8569f09fad 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -4,11 +4,13 @@ use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class GitlabFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', @@ -87,8 +89,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -151,8 +153,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -193,8 +195,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", @@ -235,8 +237,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -268,8 +270,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", @@ -282,29 +284,20 @@ public function dataFormatterOutputProvider(): iterable ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new GitlabErrorFormatter(new SimpleRelativePathHelper('/data/folder')); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 48d2b2e209..19d05899f2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -2,12 +2,17 @@ namespace PHPStan\Command\ErrorFormatter; +use Nette\Utils\Json; +use PHPStan\Analyser\Error; +use PHPStan\Command\AnalysisResult; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class JsonErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', @@ -20,7 +25,7 @@ public function dataFormatterOutputProvider(): iterable "errors":0, "file_errors":0 }, - "files":[], + "files":{}, "errors": [] }', ]; @@ -63,7 +68,7 @@ public function dataFormatterOutputProvider(): iterable "errors":1, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error" ] @@ -101,14 +106,15 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } @@ -128,10 +134,10 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -167,79 +173,83 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } }, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected - */ + #[DataProvider('dataFormatterOutputProvider')] 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()); } - /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, 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 static 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, tip: $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 cbe281f7ad..6c070585ac 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -4,15 +4,17 @@ use DOMDocument; use Generator; +use Override; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class JunitErrorFormatterTest extends ErrorFormatterTestCase { - /** @var \PHPStan\Command\ErrorFormatter\JunitErrorFormatter */ - private $formatter; + private JunitErrorFormatter $formatter; + #[Override] public function setUp(): void { parent::setUp(); @@ -21,16 +23,16 @@ public function setUp(): void } /** - * @return \Generator> + * @return Generator> */ - public function dataFormatterOutputProvider(): Generator + public static function dataFormatterOutputProvider(): Generator { yield 'No errors' => [ 0, 0, 0, ' - + ', @@ -75,7 +77,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -94,7 +96,7 @@ public function dataFormatterOutputProvider(): Generator - + ', @@ -113,7 +115,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -122,7 +124,7 @@ public function dataFormatterOutputProvider(): Generator - + ', @@ -131,23 +133,22 @@ public function dataFormatterOutputProvider(): Generator /** * Test generated use cases for JUnit output format. - * - * @dataProvider dataFormatterOutputProvider */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGeneralErrors, - string $expected + string $expected, ): void { $this->assertSame( $exitCode, $this->formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGeneralErrors), - $this->getOutput() + $this->getOutput(), ), - 'Response code do not match' + 'Response code do not match', ); $xml = new DOMDocument(); @@ -155,13 +156,13 @@ public function testFormatErrors( $this->assertTrue( $xml->schemaValidate(__DIR__ . '/junit-schema.xsd'), - 'Schema do not validate' + 'Schema do not validate', ); $this->assertXmlStringEqualsXmlString( $expected, $this->getOutputContent(), - 'XML do not match' + 'XML do not match', ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index 0f35ad8fda..07e3555d40 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -3,95 +3,126 @@ namespace PHPStan\Command\ErrorFormatter; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class RawErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ - 'No errors', - 0, - 0, - 0, - '', + 'message' => 'No errors', + 'exitCode' => 0, + 'numFileErrors' => 0, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '', ]; yield [ - 'One file error', - 1, - 1, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", + 'message' => 'One file error', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", ]; yield [ - 'One generic error', - 1, - 0, - 1, - '?:?:first generic error' . "\n", + 'message' => 'One generic error', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 1, + 'verbose' => false, + 'expected' => '?:?:first generic error' . "\n", ]; yield [ - 'Multiple file errors', - 1, - 4, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', ]; yield [ - 'Multiple generic errors', - 1, - 0, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n", + 'message' => 'Multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +', ]; yield [ - 'Multiple file, multiple generic errors', - 1, - 4, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file, multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', + ]; + + yield [ + 'message' => 'One file error with tip', + 'exitCode' => 1, + 'numFileErrors' => [5, 1], + '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, 1], + '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 */ + #[DataProvider('dataFormatterOutputProvider')] 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->assertSame($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 e36f959856..de43d63811 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -2,38 +2,68 @@ namespace PHPStan\Command\ErrorFormatter; +use Override; 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 PHPUnit\Framework\Attributes\DataProvider; +use function getenv; +use function putenv; +use function sprintf; class TableErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + private string|false $terminalEmulator; + + #[Override] + protected function setUp(): void + { + putenv('GITHUB_ACTIONS'); + + $this->terminalEmulator = getenv('TERMINAL_EMULATOR'); + putenv('TERMINAL_EMULATOR'); + } + + #[Override] + protected function tearDown(): void + { + putenv('COLUMNS'); + putenv('TERM_PROGRAM'); + putenv('TERMINAL_EMULATOR' . ($this->terminalEmulator !== false ? '=' . $this->terminalEmulator : '')); + } + + public static 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 @@ -41,41 +71,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 @@ -83,16 +119,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 @@ -100,73 +139,306 @@ 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, 1], + '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, 1], + 'numGenericErrors' => 0, + 'verbose' => true, + 'extraEnvVars' => [], + 'expected' => ' ------ ---------------- + Line foo.php + ------ ---------------- + 5 Foobar\Buz + 🪪 foobar.buz + 💡 a tip + ------ ---------------- + + + [ERROR] Found 1 error + +', + ]; + + yield [ + 'message' => 'Errors in JetBrains', + 'exitCode' => 1, + 'numFileErrors' => [5, 1], + 'numGenericErrors' => 1, + 'verbose' => true, + 'extraEnvVars' => ['TERMINAL_EMULATOR=JetBrains-JediTerm'], + 'expected' => ' ------ ---------------- + Line foo.php + ------ ---------------- + 5 Foobar\Buz + 🪪 foobar.buz + 💡 a tip + at foo.php:5 + ------ ---------------- + + -- --------------------- + Error + -- --------------------- + first generic error + -- --------------------- + + [ERROR] Found 2 errors + ', ]; } /** - * @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 */ + #[DataProvider('dataFormatterOutputProvider')] 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(new NullRelativePathHelper(), 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->assertSame($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message)); } public function testEditorUrlWithTrait(): void { - $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false, 'editor://%file%/%line%'); - $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], false, null, true), $this->getOutput()); + $formatter = $this->createErrorFormatter('editor://%file%/%line%'); + $error = new Error('Test', 'Foo.php (in context of trait)', 12, filePath: 'Foo.php', traitFilePath: 'Bar.php'); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); + + $this->assertStringContainsString('Bar.php', $this->getOutputContent()); + } + + public function testEditorUrlWithRelativePath(): void + { + $formatter = $this->createErrorFormatter('editor://custom/path/%relFile%/%line%'); + $error = new Error('Test', 'Foo.php', 12, filePath: 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, filePath: 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(); + } + + public function testBug13292(): void + { + putenv('COLUMNS=200'); + $formatter = $this->createErrorFormatter(null); + $formatter->formatErrors( + new AnalysisResult( + [ + new Error( + 'Parameter #1 $arrayabc of method Abcdefghijklmnopqrstuvwxyzabcdefghijk::translateAbcdefgh() expects array{status: int, error: string, date?: string}, non-empty-array given.', + 'Foo.php', + 5, + identifier: 'argument.type', + ), + ], + [], + [], + [], + [], + false, + null, + true, + 0, + false, + [], + ), + $this->getOutput(), + ); + self::expectNotToPerformAssertions(); + } + + public function testBug13317(): void + { + putenv('COLUMNS=170'); + $formatter = $this->createErrorFormatter(null); + $formatter->formatErrors( + new AnalysisResult( + [ + new Error( + 'Property bla::$error_params (non-empty-list|null) is never assigned non-empty-list so it can be removed from the property type.', + 'bla.php', + 6, + identifier: 'property.unusedType', + ), + ], + [], + [], + [], + [], + false, + null, + true, + 0, + false, + [], + ), + $this->getOutput(), + ); + $this->assertSame( + <<<'TABLE' + ------ ------------------------------------------------------------------------------------------------------------------------------------------------- + Line bla.php + ------ ------------------------------------------------------------------------------------------------------------------------------------------------- + 6 Property bla::$error_params (non-empty-list|null) is never assigned non-empty-list so it can be removed from the property type. + 🪪 property.unusedType + ------ ------------------------------------------------------------------------------------------------------------------------------------------------- + + + [ERROR] Found 1 error + + +TABLE, + $this->getOutputContent(), + ); + } + + private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitle = null): TableErrorFormatter + { + $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); - $this->assertStringContainsString('editor://Bar.php/12', $this->getOutputContent()); + return new TableErrorFormatter( + $relativePathHelper, + new SimpleRelativePathHelper(self::DIRECTORY_PATH), + new CiDetectedErrorFormatter( + new GithubErrorFormatter($relativePathHelper), + new TeamcityErrorFormatter($relativePathHelper), + ), + false, + $editorUrl, + $editorUrlTitle, + ); } } diff --git a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php index d90ec904f6..488d2ebcd6 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -5,11 +5,13 @@ use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; class TeamcityErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', @@ -17,6 +19,7 @@ public function dataFormatterOutputProvider(): iterable 0, 0, '', + '', ]; yield [ @@ -47,8 +50,8 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; @@ -59,7 +62,7 @@ public function dataFormatterOutputProvider(): iterable 2, '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] ', ]; @@ -71,42 +74,48 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] +', + ]; + + yield [ + 'One file error', + 1, + [4, 2], + 0, + '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foobar\Buz (🪪 foobar.buz)\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; } /** - * @dataProvider dataFormatterOutputProvider - * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected + * @param array{int, int}|int $numFileErrors */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); $formatter = new TeamcityErrorFormatter( - $relativePathHelper + $relativePathHelper, ); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertSame($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 ca7c8f9c2c..ff39bfc9d7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon @@ -11,7 +11,10 @@ parameters: path: WindowsNewlines.php - - message: "#^PHPDoc tag @param has invalid value \\(\\)\\: Unexpected token \"\\\\n\\\\t \\* \", expected type at offset 113$#" + message: """ + #^PHPDoc tag @param has invalid value \\(\r + \\$object\\)\\: Unexpected token "\\\\r\\\\n\\\\t \\* ", expected type at offset 113 on line 4$# + """ count: 1 path: WindowsNewlines.php diff --git a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon index 3bfe998b6e..398e241bd7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon @@ -11,7 +11,10 @@ parameters: path: UnixNewlines.php - - message: "#^PHPDoc tag @param has invalid value \\(\\)\\: Unexpected token \"\\\\r\\\\n\\\\t \\* \", expected type at offset 110$#" + message: """ + #^PHPDoc tag @param has invalid value \\( + \\$object\\)\\: Unexpected token "\\\\n\\\\t \\* ", expected type at offset 110 on line 4$# + """ count: 1 path: UnixNewlines.php diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 7646267702..2b64d9e534 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Command; +use Hoa\Compiler\Llk\Llk; +use Hoa\File\Read; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class IgnoredRegexValidatorTest extends PHPStanTestCase { - public function dataValidate(): array + public static function dataValidate(): array { return [ [ @@ -98,12 +101,48 @@ public function dataValidate(): array false, false, ], + [ + '~(a\()~', + [], + false, + false, + ], + [ + '~b\\\()~', + [], + false, + true, + ], + [ + '~(c\\\\\()~', + [], + false, + false, + ], [ '~Result of || is always true.~', [], false, true, ], + [ + '~a\||~', + [], + false, + false, + ], + [ + '~b\\\||~', + [], + false, + true, + ], + [ + '~c\\\\\||~', + [], + false, + false, + ], [ '#Method PragmaRX\Notified\Data\Repositories\Notified::firstOrCreateByEvent() should return PragmaRX\Notified\Data\Models\Notified but returns Illuminate\Database\Eloquent\Model|null#', [], @@ -114,21 +153,18 @@ public function dataValidate(): array } /** - * @dataProvider dataValidate - * @param string $regex * @param string[] $expectedTypes - * @param bool $expectedHasAnchors - * @param bool $expectAllErrorsIgnored */ + #[DataProvider('dataValidate')] 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); } } @@ -42,32 +54,25 @@ public function testExpectedFiles(): void sort($autoloadFiles); $expectedFiles = [ - 'clue/block-react/src/functions_include.php', // added to phpstan-dist/bootstrap.php '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 phpstan-dist/bootstrap.php 'myclabs/deep-copy/src/DeepCopy/deep_copy.php', // dev dependency of PHPUnit - 'phpstan/php-8-stubs/Php8StubsMap.php', - 'react/promise-stream/src/functions_include.php', // added to phpstan-dist/bootstrap.php - 'react/promise-timer/src/functions_include.php', // added to phpstan-dist/bootstrap.php + 'react/async/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'react/promise/src/functions_include.php', // added to phpstan-dist/bootstrap.php - 'ringcentral/psr7/src/functions_include.php', // added to phpstan-dist/bootstrap.php + '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/polyfill-php83/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/string/Resources/functions.php', // afaik polyfills aren't necessary ]; - $phpunitFunctions = 'phpunit/phpunit/src/Framework/Assert/Functions.php'; - if (PHP_VERSION_ID >= 70300) { - array_splice($expectedFiles, 6, 0, [ - $phpunitFunctions, - ]); - } - - $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/DependencyInjection/ConditionalTagsExtensionTest.php b/tests/PHPStan/DependencyInjection/ConditionalTagsExtensionTest.php new file mode 100644 index 0000000000..5c2b2e28d5 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/ConditionalTagsExtensionTest.php @@ -0,0 +1,35 @@ +getServicesByTag(LazyRegistry::RULE_TAG); + $enabledServices = array_map(static fn ($service) => get_class($service), $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabled::class, $enabledServices); + $this->assertContains(TestedConditionalServiceEnabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabledDisabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabledEnabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceEnabledDisabled::class, $enabledServices); + $this->assertContains(TestedConditionalServiceEnabledEnabled::class, $enabledServices); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/conditionalTags.neon', + ]; + } + +} diff --git a/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php new file mode 100644 index 0000000000..f24e65abc2 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php @@ -0,0 +1,25 @@ +assertCount(16, self::getContainer()->getParameter('ignoreErrors')); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/ignoreErrors.neon', + ]; + } + +} diff --git a/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php new file mode 100644 index 0000000000..6d8ac35b8c --- /dev/null +++ b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php @@ -0,0 +1,68 @@ + + */ + public static function dataValidateIgnoreErrors(): iterable + { + yield [ + __DIR__ . '/invalidIgnoreErrors/message-and-messages.neon', + 'An ignoreErrors entry cannot contain both message and messages fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-message.neon', + 'An ignoreErrors entry cannot contain both rawMessage and message fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-messages.neon', + 'An ignoreErrors entry cannot contain both rawMessage and messages fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/identifier-and-identifiers.neon', + 'An ignoreErrors entry cannot contain both identifier and identifiers fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/path-and-paths.neon', + 'An ignoreErrors entry cannot contain both path and paths fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/missing-main-key.neon', + 'An ignoreErrors entry must contain at least one of the following fields: message, messages, rawMessage, identifier, identifiers, path, paths.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/count-without-path.neon', + 'An ignoreErrors entry with count field must also contain path field.', + ]; + } + + #[DataProvider('dataValidateIgnoreErrors')] + public function testValidateIgnoreErrors(string $file, string $expectedMessage): void + { + self::$configFile = $file; + $this->expectExceptionMessage($expectedMessage); + self::getContainer(); + } + + public static function getAdditionalConfigFiles(): array + { + $files = [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + if (self::$configFile !== null) { + $files[] = self::$configFile; + } + + return $files; + } + +} diff --git a/tests/PHPStan/DependencyInjection/Nette/NetteContainerTest.php b/tests/PHPStan/DependencyInjection/Nette/NetteContainerTest.php new file mode 100644 index 0000000000..e7629e4f83 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/Nette/NetteContainerTest.php @@ -0,0 +1,37 @@ +expectException(MissingServiceException::class); + $container->getService('nonexistent'); + } + + public function testGetByTypeNotFoundThrows(): void + { + $container = self::getContainer(); + + $this->expectException(MissingServiceException::class); + $container->getByType(TrinaryLogic::class); + } + + public function testGetByTypeNotUniqueThrows(): void + { + $container = self::getContainer(); + + $this->expectException(MissingServiceException::class); + $container->getByType(ReflectionGetAttributesMethodReturnTypeExtension::class); + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php new file mode 100644 index 0000000000..ded54328e9 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceDisabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledDisabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledDisabled.php new file mode 100644 index 0000000000..636e786ea3 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledDisabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceDisabledDisabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledEnabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledEnabled.php new file mode 100644 index 0000000000..e758ce8efe --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledEnabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceDisabledEnabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabled.php new file mode 100644 index 0000000000..921f9a9590 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceEnabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledDisabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledDisabled.php new file mode 100644 index 0000000000..379700d04f --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledDisabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceEnabledDisabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledEnabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledEnabled.php new file mode 100644 index 0000000000..d12d4121c8 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledEnabled.php @@ -0,0 +1,25 @@ + + */ +class TestedConditionalServiceEnabledEnabled implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/tests/PHPStan/DependencyInjection/conditionalTags.neon b/tests/PHPStan/DependencyInjection/conditionalTags.neon new file mode 100644 index 0000000000..1804297a13 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/conditionalTags.neon @@ -0,0 +1,29 @@ +parameters: + enabled: true + disabled: false + +parametersSchema: + enabled: bool() + disabled: bool() + +conditionalTags: + PHPStan\DependencyInjection\TestedConditionalServiceDisabled: + phpstan.rules.rule: %disabled% + PHPStan\DependencyInjection\TestedConditionalServiceEnabled: + phpstan.rules.rule: %enabled% + PHPStan\DependencyInjection\TestedConditionalServiceDisabledDisabled: + phpstan.rules.rule: [%disabled%, %disabled%] + PHPStan\DependencyInjection\TestedConditionalServiceDisabledEnabled: + phpstan.rules.rule: [%disabled%, %enabled%] + PHPStan\DependencyInjection\TestedConditionalServiceEnabledDisabled: + phpstan.rules.rule: [%enabled%, %disabled%] + PHPStan\DependencyInjection\TestedConditionalServiceEnabledEnabled: + phpstan.rules.rule: [%enabled%, %enabled%] + +services: + - PHPStan\DependencyInjection\TestedConditionalServiceDisabled + - PHPStan\DependencyInjection\TestedConditionalServiceEnabled + - PHPStan\DependencyInjection\TestedConditionalServiceDisabledDisabled + - PHPStan\DependencyInjection\TestedConditionalServiceDisabledEnabled + - PHPStan\DependencyInjection\TestedConditionalServiceEnabledDisabled + - PHPStan\DependencyInjection\TestedConditionalServiceEnabledEnabled diff --git a/tests/PHPStan/DependencyInjection/ignoreErrors.neon b/tests/PHPStan/DependencyInjection/ignoreErrors.neon new file mode 100644 index 0000000000..c137667bb4 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/ignoreErrors.neon @@ -0,0 +1,65 @@ +parameters: + ignoreErrors: + - "#error#" + - + message: '#error#' + - + message: '#error#' + reportUnmatched: false + - + message: '#error#' + path: '/dir/*' + - + message: '#error#' + path: '/dir/*' + reportUnmatched: false + - + messages: + - '#error#' + - + messages: + - '#error#' + path: '/dir/*' + - + messages: + - '#error#' + path: '/dir/*' + reportUnmatched: false + - + message: '#error#' + paths: + - '/dir/*' + - + message: '#error#' + paths: + - '/dir/*' + reportUnmatched: false + - + messages: + - '#error#' + paths: + - '/dir/*' + - + messages: + - '#error#' + paths: + - '/dir/*' + reportUnmatched: false + - + identifiers: + - 'error.identifier' + - + identifiers: + - 'error.identifier' + path: '/dir/*' + - + identifiers: + - 'error.identifier' + paths: + - '/dir/*' + - + identifiers: + - 'error.identifier' + - 'another.identifier' + path: '/dir/*' + reportUnmatched: false diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/count-without-path.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/count-without-path.neon new file mode 100644 index 0000000000..6e712c383f --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/count-without-path.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + message: '#One#' + count: 3 diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/identifier-and-identifiers.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/identifier-and-identifiers.neon new file mode 100644 index 0000000000..a0df67723d --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/identifier-and-identifiers.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + identifier: argument.type + identifiers: [argument.type] diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/message-and-messages.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/message-and-messages.neon new file mode 100644 index 0000000000..4015170048 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/message-and-messages.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + message: '#One#' + messages: ['#Two#'] diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/missing-main-key.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/missing-main-key.neon new file mode 100644 index 0000000000..08c705b7a6 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/missing-main-key.neon @@ -0,0 +1,4 @@ +parameters: + ignoreErrors: + - + reportUnmatched: false diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/path-and-paths.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/path-and-paths.neon new file mode 100644 index 0000000000..63a543f41b --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/path-and-paths.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + path: ../IgnoreErrorsTest.php + paths: [../IgnoreErrorsTest.php] diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon new file mode 100644 index 0000000000..1aa155d135 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + rawMessage: 'One' + message: '#Two#' diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon new file mode 100644 index 0000000000..39f9515c4d --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + rawMessage: 'One' + messages: ['#Two#'] diff --git a/tests/PHPStan/File/FileExcluderTest.php b/tests/PHPStan/File/FileExcluderTest.php index ae415683c4..02aff99453 100644 --- a/tests/PHPStan/File/FileExcluderTest.php +++ b/tests/PHPStan/File/FileExcluderTest.php @@ -2,29 +2,30 @@ namespace PHPStan\File; -class FileExcluderTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class FileExcluderTest extends PHPStanTestCase { /** - * @dataProvider dataExcludeOnWindows - * @param string $filePath * @param string[] $analyseExcludes - * @param bool $isExcluded */ + #[DataProvider('dataExcludeOnWindows')] public function testFilesAreExcludedFromAnalysingOnWindows( string $filePath, array $analyseExcludes, - bool $isExcluded + bool $isExcluded, ): void { $this->skipIfNotOnWindows(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, []); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } - public function dataExcludeOnWindows(): array + public static function dataExcludeOnWindows(): array { return [ [ @@ -34,7 +35,7 @@ public function dataExcludeOnWindows(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -64,7 +65,7 @@ public function dataExcludeOnWindows(): array ], [ __DIR__ . '\data\parse-error.php', - ['tests/PHPStan/File/data'], + ['*/tests/PHPStan/File/data/*'], true, ], [ @@ -99,7 +100,7 @@ public function dataExcludeOnWindows(): array ], [ 'c:\etc\phpstan\dummy-1.php', - ['c:\etc\phpstan\\'], + ['c:\etc\phpstan\\*'], true, ], [ @@ -109,32 +110,30 @@ public function dataExcludeOnWindows(): array ], [ 'c:\etc\phpstan-test\dummy-2.php', - ['c:\etc\phpstan'], + ['c:\etc\phpstan*'], true, ], ]; } /** - * @dataProvider dataExcludeOnUnix - * @param string $filePath * @param string[] $analyseExcludes - * @param bool $isExcluded */ + #[DataProvider('dataExcludeOnUnix')] 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)); } - public function dataExcludeOnUnix(): array + public static function dataExcludeOnUnix(): array { return [ [ @@ -144,7 +143,7 @@ public function dataExcludeOnUnix(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -172,11 +171,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 +188,7 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan/dummy-1.php', - ['/etc/phpstan/'], + ['/etc/phpstan/*'], true, ], [ @@ -204,10 +198,62 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan-test/dummy-2.php', - ['/etc/phpstan'], + ['/etc/phpstan*'], true, ], ]; } + public static 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, + ]; + } + + /** + * @param string[] $analyseExcludes + */ + #[DataProvider('dataNoImplicitWildcard')] + 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 4082896b25..ac2cef5fd1 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -2,13 +2,16 @@ namespace PHPStan\File; -class FileHelperTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class FileHelperTest extends PHPStanTestCase { /** * @return string[][] */ - public function dataAbsolutizePathOnWindows(): array + public static function dataAbsolutizePathOnWindows(): array { return [ ['C:/Program Files', 'C:/Program Files'], @@ -18,14 +21,12 @@ 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 - */ + #[DataProvider('dataAbsolutizePathOnWindows')] public function testAbsolutizePathOnWindows(string $path, string $absolutePath): void { $this->skipIfNotOnWindows(); @@ -36,7 +37,7 @@ public function testAbsolutizePathOnWindows(string $path, string $absolutePath): /** * @return string[][] */ - public function dataAbsolutizePathOnLinuxOrMac(): array + public static function dataAbsolutizePathOnLinuxOrMac(): array { return [ ['C:/Program Files', '/abcd/C:/Program Files'], @@ -47,14 +48,12 @@ 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 - */ + #[DataProvider('dataAbsolutizePathOnLinuxOrMac')] public function testAbsolutizePathOnLinuxOrMac(string $path, string $absolutePath): void { $this->skipIfNotOnUnix(); @@ -65,7 +64,7 @@ public function testAbsolutizePathOnLinuxOrMac(string $path, string $absolutePat /** * @return string[][] */ - public function dataNormalizePathOnWindows(): array + public static function dataNormalizePathOnWindows(): array { return [ ['C:/Program Files/PHP', 'C:\Program Files\PHP'], @@ -75,14 +74,11 @@ 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 - */ + #[DataProvider('dataNormalizePathOnWindows')] public function testNormalizePathOnWindows(string $path, string $normalizedPath): void { $this->skipIfNotOnWindows(); @@ -92,7 +88,7 @@ public function testNormalizePathOnWindows(string $path, string $normalizedPath) /** * @return string[][] */ - public function dataNormalizePathOnLinuxOrMac(): array + public static function dataNormalizePathOnLinuxOrMac(): array { return [ ['C:\Program Files\PHP', 'C:/Program Files/PHP'], @@ -102,16 +98,13 @@ 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'], ]; } - /** - * @dataProvider dataNormalizePathOnLinuxOrMac - * @param string $path - * @param string $normalizedPath - */ + #[DataProvider('dataNormalizePathOnLinuxOrMac')] public function testNormalizePathOnLinuxOrMac(string $path, string $normalizedPath): void { $this->skipIfNotOnUnix(); diff --git a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php index bd0cb74822..4194588890 100644 --- a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php +++ b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php @@ -2,12 +2,13 @@ namespace PHPStan\File; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ParentDirectoryRelativePathHelperTest extends TestCase { - public function dataGetRelativePath(): array + public static function dataGetRelativePath(): array { return [ [ @@ -103,22 +104,17 @@ public function dataGetRelativePath(): array ]; } - /** - * @dataProvider dataGetRelativePath - * @param string $parentDirectory - * @param string $filename - * @param string $expectedRelativePath - */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePath( string $parentDirectory, string $filename, - string $expectedRelativePath + string $expectedRelativePath, ): void { $helper = new ParentDirectoryRelativePathHelper($parentDirectory); $this->assertSame( $expectedRelativePath, - $helper->getRelativePath($filename) + $helper->getRelativePath($filename), ); } diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 7ede64384d..7c484879cc 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -2,10 +2,16 @@ namespace PHPStan\File; -class RelativePathHelperTest extends \PHPUnit\Framework\TestCase +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use function array_map; +use function str_replace; +use function substr; + +class RelativePathHelperTest extends TestCase { - public function dataGetRelativePath(): array + public static function dataGetRelativePath(): array { return [ [ @@ -137,42 +143,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 */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePathOnUnix( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '/'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } /** - * @dataProvider dataGetRelativePath - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePathOnWindows( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $sanitize = static function (string $path): string { @@ -185,11 +219,11 @@ public function testGetRelativePathOnWindows( $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $sanitize($currentWorkingDirectory), array_map($sanitize, $analysedPaths), '\\'); $this->assertSame( $sanitize($expectedResult), - $helper->getRelativePath($sanitize($filenameToRelativize)) + $helper->getRelativePath($sanitize($filenameToRelativize)), ); } - public function dataGetRelativePathWindowsSpecific(): array + public static function dataGetRelativePathWindowsSpecific(): array { return [ [ @@ -214,23 +248,20 @@ public function dataGetRelativePathWindowsSpecific(): array } /** - * @dataProvider dataGetRelativePathWindowsSpecific - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ + #[DataProvider('dataGetRelativePathWindowsSpecific')] public function testGetRelativePathWindowsSpecific( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '\\'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } diff --git a/tests/PHPStan/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..e8309a0846 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -2,13 +2,14 @@ namespace PHPStan\Generics; -/** - * @group exec - */ -class GenericsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; + +#[Group('levels')] +class GenericsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['functions'], @@ -17,11 +18,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 e2b36d0a05..20be5000fc 100644 --- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php +++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php @@ -2,23 +2,27 @@ namespace PHPStan\Generics; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerType; +use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class TemplateTypeFactoryTest extends \PHPStan\Testing\PHPStanTestCase +class TemplateTypeFactoryTest extends PHPStanTestCase { /** @return array */ - public function dataCreate(): array + public static function dataCreate(): array { return [ [ @@ -50,9 +54,14 @@ public function dataCreate(): array TemplateTypeScope::createWithFunction('a'), 'U', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'U', + null, + TemplateTypeVariance::createInvariant(), ), - new MixedType(), ], [ new UnionType([ @@ -64,12 +73,14 @@ public function dataCreate(): array new IntegerType(), ]), ], + [ + new IterableType(new IntegerType(), new StringType()), + new IterableType(new IntegerType(), new StringType()), + ], ]; } - /** - * @dataProvider dataCreate - */ + #[DataProvider('dataCreate')] public function testCreate(?Type $bound, Type $expectedBound): void { $scope = TemplateTypeScope::createWithFunction('a'); @@ -77,12 +88,12 @@ public function testCreate(?Type $bound, Type $expectedBound): void $scope, 'T', $bound, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); $this->assertTrue( $expectedBound->equals($templateType->getBound()), - sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())) + sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Generics/data/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/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/typeProjections-0.json b/tests/PHPStan/Generics/data/typeProjections-0.json new file mode 100644 index 0000000000..548c221a62 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-0.json @@ -0,0 +1,37 @@ +[ + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 38, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 48, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\A", + "line": 56, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 65, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 91, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 105, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 119, + "ignorable": false + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections-5.json b/tests/PHPStan/Generics/data/typeProjections-5.json new file mode 100644 index 0000000000..ada4b861f0 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-5.json @@ -0,0 +1,52 @@ +[ + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects never, PHPStan\\Generics\\TypeProjections\\B given.", + "line": 37, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects PHPStan\\Generics\\TypeProjections\\B, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 46, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box<*>::pack() expects never, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 64, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 94, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 95, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 96, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): PHPStan\\Generics\\TypeProjections\\B, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 108, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 122, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 123, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 124, + "ignorable": true + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections.php b/tests/PHPStan/Generics/data/typeProjections.php new file mode 100644 index 0000000000..555c1a50ae --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections.php @@ -0,0 +1,125 @@ + $box + */ +function testCovariant(Box $box, B $b): void +{ + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param Box $box + */ +function testContravariant(Box $box, A $a, B $b): void +{ + $box->pack($a); + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param BoundedBox $box + */ +function testContravariantWithBound(BoundedBox $box): void +{ + dumpType($box->unpack()); +} + +/** + * @param Box<*> $box + */ +function testStar(Box $box, A $a): void +{ + $box->pack($a); + dumpType($box->unpack()); +} + + +/** + * @template T + */ +interface Mapped +{ + /** + * @param callable(T): void $mapper + */ + public function mapIn(callable $mapper): void; + + /** + * @param callable(): T $mapper + */ + public function mapOut(callable $mapper): void; +} + +/** + * @param Mapped $mapped + */ +function testCovariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped $mapped + */ +function testContravariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped<*> $mapped + */ +function testStarMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index 888e38af9c..3c7d9da4d4 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -60,13 +60,18 @@ "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::__construct().", - "line": 142, + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter t of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 154, + "ignorable": true + }, + { + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter w of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 154, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", - "line": 153, + "line": 154, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-4.json b/tests/PHPStan/Generics/data/variance-4.json index c6fa368103..7757cc3dea 100644 --- a/tests/PHPStan/Generics/data/variance-4.json +++ b/tests/PHPStan/Generics/data/variance-4.json @@ -1,7 +1,7 @@ [ { "message": "Property PHPStan\\Generics\\Variance\\ConstructorAndStatic::$data is never read, only written.", - "line": 134, + "line": 135, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-5.json b/tests/PHPStan/Generics/data/variance-5.json index 016235e900..a16b075630 100644 --- a/tests/PHPStan/Generics/data/variance-5.json +++ b/tests/PHPStan/Generics/data/variance-5.json @@ -1,7 +1,7 @@ [ { "message": "Parameter #1 $it of function PHPStan\\Generics\\Variance\\acceptInvariantIterOfDateTimeInterface expects PHPStan\\Generics\\Variance\\InvariantIter, PHPStan\\Generics\\Variance\\InvariantIter given.", - "line": 164, + "line": 165, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance.php b/tests/PHPStan/Generics/data/variance.php index defa4910d3..8b68847f77 100644 --- a/tests/PHPStan/Generics/data/variance.php +++ b/tests/PHPStan/Generics/data/variance.php @@ -127,6 +127,7 @@ function set($v): void; /** * @template-covariant T * @template U + * @phpstan-consistent-constructor */ class ConstructorAndStatic { @@ -151,7 +152,7 @@ public function __construct($t, $u, $v, $w) { * @return Static */ public static function create($t, $u, $v, $w) { - return new self($t, $u, $v, $w); + return new static($t, $u, $v, $w); } } diff --git a/tests/PHPStan/Internal/ArrayHelperTest.php b/tests/PHPStan/Internal/ArrayHelperTest.php new file mode 100644 index 0000000000..6cf63b46a7 --- /dev/null +++ b/tests/PHPStan/Internal/ArrayHelperTest.php @@ -0,0 +1,61 @@ + [ + 'dep2a' => [ + 'dep3a' => null, + ], + 'dep2b' => null, + ], + 'dep1b' => null, + ]; + + ArrayHelper::unsetKeyAtPath($array, ['dep1a', 'dep2a', 'dep3a']); + + $this->assertSame([ + 'dep1a' => [ + 'dep2a' => [], + 'dep2b' => null, + ], + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1a', 'dep2a']); + + $this->assertSame([ + 'dep1a' => [ + 'dep2b' => null, + ], + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1a']); + + $this->assertSame([ + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1b']); + + $this->assertSame([], $array); + } + + public function testUnsetKeyAtPathEmpty(): void + { + $array = []; + + ArrayHelper::unsetKeyAtPath($array, ['foo', 'bar']); + + $this->assertSame([], $array); + } + +} diff --git a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php index c3dacc310b..3338348583 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -2,13 +2,14 @@ namespace PHPStan\Levels; -/** - * @group exec - */ -class InferPrivatePropertyTypeFromConstructorIntegrationTest extends \PHPStan\Testing\LevelsTestCase +use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; + +#[Group('levels')] +class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['inferPropertyType'], diff --git a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php b/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php deleted file mode 100644 index 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 index 6f7aaf4dad..999427f6c6 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -2,13 +2,14 @@ namespace PHPStan\Levels; -/** - * @group exec - */ -class NamedArgumentsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; + +#[Group('levels')] +class NamedArgumentsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['namedArguments'], @@ -27,7 +28,7 @@ public function getPhpStanExecutablePath(): string public function getPhpStanConfigPath(): string { - return __DIR__ . '/staticReflection.neon'; + return __DIR__ . '/namedArguments.neon'; } protected function shouldAutoloadAnalysedFile(): bool diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index 80d171f7db..d76d9045c0 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -2,13 +2,14 @@ namespace PHPStan\Levels; -/** - * @group exec - */ -class StubValidatorIntegrationTest extends \PHPStan\Testing\LevelsTestCase +use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; + +#[Group('levels')] +class StubValidatorIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['stubValidator'], diff --git a/tests/PHPStan/Levels/StubsIntegrationTest.php b/tests/PHPStan/Levels/StubsIntegrationTest.php index d157ec70e1..c8be90cd53 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -2,13 +2,14 @@ namespace PHPStan\Levels; -/** - * @group exec - */ -class StubsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; + +#[Group('levels')] +class StubsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { require_once __DIR__ . '/data/stubs-functions.php'; diff --git a/tests/PHPStan/Levels/alwaysTrue.neon b/tests/PHPStan/Levels/alwaysTrue.neon deleted file mode 100644 index b385d1c956..0000000000 --- a/tests/PHPStan/Levels/alwaysTrue.neon +++ /dev/null @@ -1,7 +0,0 @@ -includes: - - ../../../conf/bleedingEdge.neon - -parameters: - checkAlwaysTrueCheckTypeFunctionCall: true - checkAlwaysTrueInstanceof: true - checkAlwaysTrueStrictComparison: true diff --git a/tests/PHPStan/Levels/data/acceptTypes-10.json b/tests/PHPStan/Levels/data/acceptTypes-10.json new file mode 100644 index 0000000000..8a1b7a3992 --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-10.json @@ -0,0 +1,17 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 170, + "ignorable": true + }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", + "line": 325, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", + "line": 326, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-4.json b/tests/PHPStan/Levels/data/acceptTypes-4.json new file mode 100644 index 0000000000..fbcb96fc5a --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-4.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 531, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 532, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 542, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 543, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 78f19787d7..4bb076e055 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -60,7 +60,7 @@ "ignorable": true }, { - "message": "Parameter #1 $var of function count expects array|Countable, string given.", + "message": "Parameter #1 $value of function count expects array|Countable, string given.", "line": 170, "ignorable": true }, @@ -94,16 +94,6 @@ "line": 251, "ignorable": true }, - { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 319, - "ignorable": true - }, - { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 320, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float given.", "line": 412, @@ -140,30 +130,35 @@ "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 579, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array() given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{} given.", "line": 580, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 1) given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 1} given.", "line": 582, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 'nonexistent') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 'nonexistent'} given.", "line": 584, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('bar' => 'date') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{bar: 'date'} given.", "line": 585, "ignorable": true }, + { + "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, @@ -180,28 +175,18 @@ "ignorable": true }, { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, 'foo' given.", + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, 'foo' given.", "line": 707, "ignorable": true }, { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, string given.", - "line": 708, - "ignorable": true - }, - { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array() given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array given.", - "line": 735, - "ignorable": true - }, - { - "message": "Parameter #2 $pieces of function implode expects array, int given.", + "message": "Parameter #2 $array of function implode expects array, int given.", "line": 763, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 4dd170abf9..216fad8987 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,30 +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, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 283, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 284, "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 301, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 302, "ignorable": true }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 319, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 320, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", "line": 415, @@ -80,7 +85,7 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 531, "ignorable": true }, @@ -90,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 }, @@ -134,11 +139,6 @@ "line": 647, "ignorable": true }, - { - "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.", - "line": 648, - "ignorable": true - }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", "line": 690, @@ -155,8 +155,18 @@ "ignorable": true }, { - "message": "Parameter #2 $pieces of function implode expects array, array|int|string given.", + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", + "line": 708, + "ignorable": true + }, + { + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "line": 735, + "ignorable": true + }, + { + "message": "Parameter #2 $array of function implode expects array, array|int|string given.", "line": 756, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/acceptTypes-8.json b/tests/PHPStan/Levels/data/acceptTypes-8.json index 49eddfd569..10728d1f25 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-8.json +++ b/tests/PHPStan/Levels/data/acceptTypes-8.json @@ -14,6 +14,11 @@ "line": 91, "ignorable": true }, + { + "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", + "line": 131, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, int|null given.", "line": 414, @@ -28,5 +33,15 @@ "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", "line": 495, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test1() should return int but returns int|null.", + "line": 771, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test2() should return array but returns array.", + "line": 779, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index d16184f062..61b2c1fbbb 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -763,3 +763,19 @@ public function invalidType($invalid) { $imploded = implode('abc', $invalid); } } + +class Discussion8209 +{ + public function test1(?int $id): int + { + return $id; + } + + /** + * @return array + */ + public function test2(?int $id): array + { + return [$id]; + } +} diff --git a/tests/PHPStan/Levels/data/arrayAccess-10.json b/tests/PHPStan/Levels/data/arrayAccess-10.json new file mode 100644 index 0000000000..9dc3ca3eb9 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayAccess-10.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot assign offset mixed to SplObjectStorage.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayAccess.php b/tests/PHPStan/Levels/data/arrayAccess.php index b77266e0f6..b07e6930b7 100644 --- a/tests/PHPStan/Levels/data/arrayAccess.php +++ b/tests/PHPStan/Levels/data/arrayAccess.php @@ -44,3 +44,17 @@ public function doLorem( } } + +/** + * @return mixed[] + */ +function bug12931():array { + /** @var array> $data */ + $data = []; + $data['attr'] = []; + $data['attr']['first'] = 1; + $data['attr']['second'] = 2; + $data['attr']['third'] = 3; + + return $data; +} diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-3.json b/tests/PHPStan/Levels/data/arrayDestructuring-3.json index 710133680d..a97c71f0f5 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-3.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-3.json @@ -5,7 +5,7 @@ "ignorable": true }, { - "message": "Offset 3 does not exist on array('a', 'b', 'c').", + "message": "Offset 3 does not exist on array{'a', 'b', 'c'}.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json index 7842bce806..3b033abd6d 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-8.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot use array destructuring on array|null.", + "message": "Cannot use array destructuring on array|null.", "line": 15, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDestructuring.php b/tests/PHPStan/Levels/data/arrayDestructuring.php index 5d7e61ea93..c97a3e6328 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring.php +++ b/tests/PHPStan/Levels/data/arrayDestructuring.php @@ -1,6 +1,6 @@ 1).", + "message": "Offset 'b' does not exist on array{a: 1}.", "line": 21, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 15e551e874..23df32943a 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -5,17 +5,17 @@ "ignorable": true }, { - "message": "Cannot access offset 'a' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 'a' on array{a: 1}|stdClass.", "line": 27, "ignorable": true }, { - "message": "Cannot access offset 'b' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 'b' on array{a: 1}|stdClass.", "line": 28, "ignorable": true }, { - "message": "Offset 'b' does not exist on array('a' => 1, ?'b' => 1).", + "message": "Offset 'b' might not exist on array{a: 1, b?: 1}.", "line": 40, "ignorable": true }, @@ -28,5 +28,10 @@ "message": "Cannot access offset 'foo' on iterable.", "line": 58, "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on iterable.", + "line": 66, + "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-8.json b/tests/PHPStan/Levels/data/arrayDimFetches-8.json index 9f4c150113..e7e6efcd13 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-8.json @@ -1,12 +1,12 @@ [ { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 15, "ignorable": true }, { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 50, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDimFetches.php b/tests/PHPStan/Levels/data/arrayDimFetches.php index effe56842c..269128a93e 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches.php +++ b/tests/PHPStan/Levels/data/arrayDimFetches.php @@ -54,6 +54,14 @@ public function doBaz($a, $b): void * @param iterable $iterable */ public function iterableOffset($iterable): void + { + var_dump($iterable['foo']); + } + + /** + * @param iterable $iterable + */ + public function iterableOffsetWithUnset($iterable): void { unset($iterable['foo']); } diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json new file mode 100644 index 0000000000..e0b5d1d2d8 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json @@ -0,0 +1,52 @@ +[ + { + "message": "Possibly invalid array key type mixed.", + "line": 22, + "ignorable": true + }, + { + "message": "Possibly invalid array key type mixed.", + "line": 31, + "ignorable": true + }, + { + "message": "Cannot access offset 42 on mixed.", + "line": 42, + "ignorable": true + }, + { + "message": "Cannot access offset null on mixed.", + "line": 43, + "ignorable": true + }, + { + "message": "Cannot access offset DateTimeImmutable on mixed.", + "line": 44, + "ignorable": true + }, + { + "message": "Cannot access offset int|null on mixed.", + "line": 45, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 46, + "ignorable": true + }, + { + "message": "Cannot access offset object|null on mixed.", + "line": 47, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 48, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 49, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json new file mode 100644 index 0000000000..3e23caff09 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json @@ -0,0 +1,7 @@ +[ + { + "message": "Invalid array key type DateTimeImmutable.", + "line": 17, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json new file mode 100644 index 0000000000..3f91016ab3 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json @@ -0,0 +1,142 @@ +[ + { + "message": "Expression \"$a[42]\" on a separate line does not do anything.", + "line": 15, + "ignorable": true + }, + { + "message": "Expression \"$a[null]\" on a separate line does not do anything.", + "line": 16, + "ignorable": true + }, + { + "message": "Expression \"$a[$intOrNull]\" on a separate line does not do anything.", + "line": 18, + "ignorable": true + }, + { + "message": "Expression \"$a[$objectOrInt]\" on a separate line does not do anything.", + "line": 19, + "ignorable": true + }, + { + "message": "Expression \"$a[$objectOrNull]\" on a separate line does not do anything.", + "line": 20, + "ignorable": true + }, + { + "message": "Expression \"$a[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 21, + "ignorable": true + }, + { + "message": "Expression \"$a[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 22, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[42]\" on a separate line does not do anything.", + "line": 24, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[null]\" on a separate line does not do anything.", + "line": 25, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$intOrNull]\" on a separate line does not do anything.", + "line": 27, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$objectOrInt]\" on a separate line does not do anything.", + "line": 28, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$objectOrNull]\" on a separate line does not do anything.", + "line": 29, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 30, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 31, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[42]\" on a separate line does not do anything.", + "line": 33, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[null]\" on a separate line does not do anything.", + "line": 34, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$intOrNull]\" on a separate line does not do anything.", + "line": 36, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$objectOrInt]\" on a separate line does not do anything.", + "line": 37, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$objectOrNull]\" on a separate line does not do anything.", + "line": 38, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 39, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 40, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[42]\" on a separate line does not do anything.", + "line": 42, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[null]\" on a separate line does not do anything.", + "line": 43, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$intOrNull]\" on a separate line does not do anything.", + "line": 45, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$objectOrInt]\" on a separate line does not do anything.", + "line": 46, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$objectOrNull]\" on a separate line does not do anything.", + "line": 47, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 48, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 49, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json new file mode 100644 index 0000000000..5c3bc3d299 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json @@ -0,0 +1,22 @@ +[ + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $a with no value type specified in iterable type array.", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $arrayOrObject with generic interface ArrayAccess but does not specify its types: TKey, TValue", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $arrayOrObject with no value type specified in iterable type array.", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $implicitlyMixed with no type specified.", + "line": 13, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json new file mode 100644 index 0000000000..674057be06 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json @@ -0,0 +1,27 @@ +[ + { + "message": "Possibly invalid array key type int|object.", + "line": 19, + "ignorable": true + }, + { + "message": "Possibly invalid array key type object|null.", + "line": 20, + "ignorable": true + }, + { + "message": "Possibly invalid array key type DateTimeImmutable.", + "line": 26, + "ignorable": true + }, + { + "message": "Possibly invalid array key type int|object.", + "line": 28, + "ignorable": true + }, + { + "message": "Possibly invalid array key type object|null.", + "line": 29, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json new file mode 100644 index 0000000000..edca311940 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json @@ -0,0 +1,52 @@ +[ + { + "message": "Possibly invalid array key type mixed.", + "line": 21, + "ignorable": true + }, + { + "message": "Possibly invalid array key type mixed.", + "line": 30, + "ignorable": true + }, + { + "message": "Cannot access offset 42 on mixed.", + "line": 33, + "ignorable": true + }, + { + "message": "Cannot access offset null on mixed.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot access offset DateTimeImmutable on mixed.", + "line": 35, + "ignorable": true + }, + { + "message": "Cannot access offset int|null on mixed.", + "line": 36, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 37, + "ignorable": true + }, + { + "message": "Cannot access offset object|null on mixed.", + "line": 38, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 39, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 40, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess.php b/tests/PHPStan/Levels/data/arrayOffsetAccess.php new file mode 100644 index 0000000000..362bb5c03f --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess.php @@ -0,0 +1,51 @@ +|(callable(): mixed) to int.", "line": 20, "ignorable": true }, { - "message": "Cannot cast array|float|int to string.", + "message": "Cannot cast array|float|int to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/clone-10-missing.json b/tests/PHPStan/Levels/data/clone-10-missing.json new file mode 100644 index 0000000000..40e1203120 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-10-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot clone non-object variable $nullableInt of type int.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone.php b/tests/PHPStan/Levels/data/clone.php index 6bd72cfe38..4697debb13 100644 --- a/tests/PHPStan/Levels/data/clone.php +++ b/tests/PHPStan/Levels/data/clone.php @@ -26,14 +26,14 @@ public function doFoo( $mixed ) { - clone $int; - clone $intOrString; - clone $foo; - clone $nullableFoo; - clone $fooOrInt; - clone $nullableInt; - clone $nullableUnion; - clone $mixed; + $result = clone $int; + $result = clone $intOrString; + $result = clone $foo; + $result = clone $nullableFoo; + $result = clone $fooOrInt; + $result = clone $nullableInt; + $result = clone $nullableUnion; + $result = clone $mixed; } } diff --git a/tests/PHPStan/Levels/data/coalesce-10.json b/tests/PHPStan/Levels/data/coalesce-10.json new file mode 100644 index 0000000000..e74887cb13 --- /dev/null +++ b/tests/PHPStan/Levels/data/coalesce-10.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot access property $bar on mixed.", + "line": 6, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 11, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-2.json b/tests/PHPStan/Levels/data/coalesce-2.json deleted file mode 100644 index 3f41943cf7..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-2.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Access to an undefined property ReflectionClass::$nonexistent.", - "line": 11, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-4.json b/tests/PHPStan/Levels/data/coalesce-4.json deleted file mode 100644 index 4d9dd368f4..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-4.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Property ReflectionClass::$name (class-string) on left side of ?? is not nullable.", - "line": 10, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/comparison.php b/tests/PHPStan/Levels/data/comparison.php index d208e6051c..ee10f825be 100644 --- a/tests/PHPStan/Levels/data/comparison.php +++ b/tests/PHPStan/Levels/data/comparison.php @@ -8,29 +8,29 @@ class Foo private const FOO_CONST = 'foo'; /** - * @param \stdClass $object + * @param \stdClass $object * @param int $int - * @param float $float + * @param float $float * @param string $string * @param int|string $intOrString * @param int|\stdClass $intOrObject */ public function doFoo( - \stdClass $object, + \stdClass $object, int $int, float $float, string $string, $intOrString, - $intOrObject + $intOrObject ) { - $object == $int; - $object == $float; - $object == $string; - $object == $intOrString; - $object == $intOrObject; + $result = $object == $int; + $result = $object == $float; + $result = $object == $string; + $result = $object == $intOrString; + $result = $object == $intOrObject; - self::FOO_CONST === 'bar'; + $result = self::FOO_CONST === 'bar'; } public function doBar(\ffmpeg_movie $movie): void diff --git a/tests/PHPStan/Levels/data/constantAccesses-10-missing.json b/tests/PHPStan/Levels/data/constantAccesses-10-missing.json new file mode 100644 index 0000000000..0cc5a3f5d4 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-10-missing.json @@ -0,0 +1,17 @@ +[ + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 53, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 56, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::FOO_CONSTANT.", + "line": 55, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses-10.json b/tests/PHPStan/Levels/data/constantAccesses-10.json new file mode 100644 index 0000000000..cf84dbb4c6 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-10.json @@ -0,0 +1,62 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 6, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 17, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 20, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 23, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 49, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 50, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 52, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 53, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 55, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 56, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 58, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-10.json b/tests/PHPStan/Levels/data/constantAccesses83-10.json new file mode 100644 index 0000000000..7d5fcb38d3 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-10.json @@ -0,0 +1,27 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 15, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 16, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 19, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 20, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-2.json b/tests/PHPStan/Levels/data/constantAccesses83-2.json new file mode 100644 index 0000000000..dae088dd65 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-2.json @@ -0,0 +1,12 @@ +[ + { + "message": "Class constant name for Levels\\ConstantAccesses83\\Foo must be a string, but int was given.", + "line": 18, + "ignorable": true + }, + { + "message": "Class constant name in dynamic fetch can only be a string, int given.", + "line": 18, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/constantAccesses83-7.json b/tests/PHPStan/Levels/data/constantAccesses83-7.json new file mode 100644 index 0000000000..6bca19187e --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-7.json @@ -0,0 +1,12 @@ +[ + { + "message": "Class constant name for Levels\\ConstantAccesses83\\Foo must be a string, but int|string was given.", + "line": 19, + "ignorable": true + }, + { + "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..f6136344ba --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-8.json @@ -0,0 +1,12 @@ +[ + { + "message": "Class constant name for Levels\\ConstantAccesses83\\Foo must be a string, but string|null was given.", + "line": 20, + "ignorable": true + }, + { + "message": "Class constant name in dynamic fetch can only be a string, string|null given.", + "line": 20, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83.php b/tests/PHPStan/Levels/data/constantAccesses83.php new file mode 100644 index 0000000000..ceb192d073 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83.php @@ -0,0 +1,21 @@ += 8.3 + +namespace Levels\ConstantAccesses83; + +class Foo +{ + + public const FOO_CONSTANT = 'foo'; + +} + +function (Foo $foo, string $a, int $i, int|string $is, ?string $sn): void { + echo Foo::FOO_CONSTANT; + + echo Foo::{$a}; + echo $foo::{$a}; + + echo Foo::{$i}; + echo Foo::{$is}; + echo Foo::{$sn}; +}; diff --git a/tests/PHPStan/Levels/data/echo_-2.json b/tests/PHPStan/Levels/data/echo_-2.json index 1c35f8ef7c..330e326e8e 100644 --- a/tests/PHPStan/Levels/data/echo_-2.json +++ b/tests/PHPStan/Levels/data/echo_-2.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter #1 (array) of echo cannot be converted to string.", + "message": "Parameter #1 (array) of echo cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", + "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/echo_-7.json b/tests/PHPStan/Levels/data/echo_-7.json index 3f32efd774..275583f027 100644 --- a/tests/PHPStan/Levels/data/echo_-7.json +++ b/tests/PHPStan/Levels/data/echo_-7.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", + "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter #4 (array|string) of echo cannot be converted to string.", + "message": "Parameter #4 (array|string) of echo cannot be converted to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/encapsedString-2.json b/tests/PHPStan/Levels/data/encapsedString-2.json index 01ea01b977..8035809eb6 100644 --- a/tests/PHPStan/Levels/data/encapsedString-2.json +++ b/tests/PHPStan/Levels/data/encapsedString-2.json @@ -1,11 +1,11 @@ [ { - "message": "Part $array (array) of encapsed string cannot be cast to string.", + "message": "Part $array (array) of encapsed string cannot be cast to string.", "line": 21, "ignorable": true }, { - "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", "line": 22, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/encapsedString-7.json b/tests/PHPStan/Levels/data/encapsedString-7.json index 577b87c127..c468ffbc0a 100644 --- a/tests/PHPStan/Levels/data/encapsedString-7.json +++ b/tests/PHPStan/Levels/data/encapsedString-7.json @@ -1,11 +1,11 @@ [ { - "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", "line": 23, "ignorable": true }, { - "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", "line": 24, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/iterable-7.json b/tests/PHPStan/Levels/data/iterable-7.json index 74440d64d8..5c3c24924d 100644 --- a/tests/PHPStan/Levels/data/iterable-7.json +++ b/tests/PHPStan/Levels/data/iterable-7.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", "line": 35, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/iterable-8.json b/tests/PHPStan/Levels/data/iterable-8.json index 17e368b276..0a46f4b75e 100644 --- a/tests/PHPStan/Levels/data/iterable-8.json +++ b/tests/PHPStan/Levels/data/iterable-8.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", "line": 26, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/listType-3.json b/tests/PHPStan/Levels/data/listType-3.json new file mode 100644 index 0000000000..c25a0ea723 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-3.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 24, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-7.json b/tests/PHPStan/Levels/data/listType-7.json new file mode 100644 index 0000000000..620ac9317f --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-7.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 25, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-8.json b/tests/PHPStan/Levels/data/listType-8.json new file mode 100644 index 0000000000..388a730546 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept list.", + "line": 26, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType.php b/tests/PHPStan/Levels/data/listType.php new file mode 100644 index 0000000000..2123effeef --- /dev/null +++ b/tests/PHPStan/Levels/data/listType.php @@ -0,0 +1,30 @@ + */ + public $list; + + /** + * @param array $stringKeyArray + * @param array $intKeyArray + * @param list $stringOrNullList + * @param list $stringList + */ + public function doFoo( + array $stringKeyArray, + array $intKeyArray, + array $stringOrNullList, + array $stringList + ): void + { + $this->list = $stringKeyArray; + $this->list = $intKeyArray; + $this->list = $stringOrNullList; + $this->list = $stringList; + } + +} diff --git a/tests/PHPStan/Levels/data/methodCalls-10-missing.json b/tests/PHPStan/Levels/data/methodCalls-10-missing.json new file mode 100644 index 0000000000..47cdcab769 --- /dev/null +++ b/tests/PHPStan/Levels/data/methodCalls-10-missing.json @@ -0,0 +1,52 @@ +[ + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 53, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 56, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 59, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 162, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 166, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 59, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 60, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 171, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/missingTypes-6.json b/tests/PHPStan/Levels/data/missingTypes-6.json new file mode 100644 index 0000000000..66b17f1111 --- /dev/null +++ b/tests/PHPStan/Levels/data/missingTypes-6.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class MissingTypesLevels\\Foo extends generic class MissingTypesLevels\\Generic but does not specify its types: T", + "line": 13, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/missingTypes.php b/tests/PHPStan/Levels/data/missingTypes.php new file mode 100644 index 0000000000..ed3d9bd3bf --- /dev/null +++ b/tests/PHPStan/Levels/data/missingTypes.php @@ -0,0 +1,16 @@ + of print cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", + "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", "line": 22, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/print_-7.json b/tests/PHPStan/Levels/data/print_-7.json index 84c94b44c4..532f660fad 100644 --- a/tests/PHPStan/Levels/data/print_-7.json +++ b/tests/PHPStan/Levels/data/print_-7.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter array|float|int of print cannot be converted to string.", + "message": "Parameter array|float|int of print cannot be converted to string.", "line": 23, "ignorable": true }, { - "message": "Parameter array|string of print cannot be converted to string.", + "message": "Parameter array|string of print cannot be converted to string.", "line": 24, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json new file mode 100644 index 0000000000..fd6f669c7c --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json @@ -0,0 +1,52 @@ +[ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 61, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 166, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", + "line": 63, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", + "line": 169, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", + "line": 170, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-10.json b/tests/PHPStan/Levels/data/propertyAccesses-10.json new file mode 100644 index 0000000000..9581e25ad9 --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-10.json @@ -0,0 +1,57 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 14, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 32, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 36, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 95, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 186, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 187, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 188, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 198, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 199, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 200, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-2.json b/tests/PHPStan/Levels/data/propertyAccesses-2.json index 95bf5c3c29..1d3376b781 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-2.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-2.json @@ -9,21 +9,41 @@ "line": 36, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", "line": 66, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", "line": 173, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-6.json b/tests/PHPStan/Levels/data/propertyAccesses-6.json index edeb6d1b42..b62abeefd8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -19,11 +19,6 @@ "line": 74, "ignorable": true }, - { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return type specified.", - "line": 83, - "ignorable": true - }, { "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 93, @@ -39,4 +34,4 @@ "line": 158, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json new file mode 100644 index 0000000000..7d9c064f4b --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json @@ -0,0 +1,22 @@ +[ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7.json b/tests/PHPStan/Levels/data/propertyAccesses-7.json index aa6291fdfe..b6df87becb 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-7.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-7.json @@ -38,5 +38,10 @@ "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", "line": 170, "ignorable": true + }, + { + "message": "Access to an undefined property object::$baz.", + "line": 200, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json index 1a8bc8b4b7..fd6f669c7c 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json @@ -1,14 +1,34 @@ [ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", "line": 63, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json index 1a8bc8b4b7..fd6f669c7c 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json @@ -1,14 +1,34 @@ [ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", "line": 63, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9.json b/tests/PHPStan/Levels/data/propertyAccesses-9.json new file mode 100644 index 0000000000..c7c6ae5a96 --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot access property $foo on mixed.", + "line": 197, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses.php b/tests/PHPStan/Levels/data/propertyAccesses.php index 72c9d4bcda..1c006ab6dc 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses.php +++ b/tests/PHPStan/Levels/data/propertyAccesses.php @@ -174,3 +174,31 @@ public function doBaz() } } + +class ObjectWithIsset +{ + + public function doFoo(): void + { + $test = new \stdClass; + + if (isset($test->foo)) { + echo $test->foo; + echo $test->bar; + echo $test->baz; + } + } + + /** + * @param mixed $test + */ + public function doBar($test): void + { + if (isset($test->foo) && isset($test->bar)) { + echo $test->foo; + echo $test->bar; + echo $test->baz; + } + } + +} diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-10.json b/tests/PHPStan/Levels/data/stringOffsetAccess-10.json new file mode 100644 index 0000000000..cc773c1e06 --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-10.json @@ -0,0 +1,32 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 13, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 16, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 23, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 27, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 31, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-2.json b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json new file mode 100644 index 0000000000..9188b4d36d --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "PHPDoc tag @var with type int|object is not subtype of native type null.", + "line": 7, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json index 9eb7139340..2b54f201d4 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json @@ -8,5 +8,15 @@ "message": "Offset 12.34 does not exist on 'foo'.", "line": 16, "ignorable": true + }, + { + "message": "Offset int|object does not exist on array{baz: 21}|array{foo: 17, bar: 19}.", + "line": 55, + "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..cfe8c12f38 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json @@ -1,12 +1,32 @@ [ { - "message": "Offset 'foo' does not exist on array|string.", + "message": "Offset int|object might not exist on 'foo'.", + "line": 19, + "ignorable": true + }, + { + "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": "Offset int|object might not exist on array|string.", + "line": 35, + "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 index bf290e21ef..1e218ab052 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-9.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-9.json @@ -4,19 +4,39 @@ "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 f56b9475af..ce13d6e997 100644 --- a/tests/PHPStan/Levels/data/stubValidator-0.json +++ b/tests/PHPStan/Levels/data/stubValidator-0.json @@ -2,31 +2,31 @@ { "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 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 type 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 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-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 d626c962a0..0000000000 --- a/tests/PHPStan/Levels/data/stubs-methods-6.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has no return type specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has parameter $i with no type specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\InterfaceWithStubPhpDoc2::doFoo() has no return type specified.", - "line": 151, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has no return type specified.", - "line": 197, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has parameter $j with no type 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/unreachable-4-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json deleted file mode 100644 index 5f0119364d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 11, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", - "line": 20, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 29, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 38, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 47, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 74, - "ignorable": true - }, - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 74, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 79, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", - "line": 79, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 84, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 84, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 89, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 94, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 102, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 102, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-4.json b/tests/PHPStan/Levels/data/unreachable-4.json index f87bfc8cea..4e6216b466 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -1,17 +1,17 @@ [ { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", + "line": 11, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, + "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", + "line": 20, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 29, "ignorable": true }, { @@ -19,6 +19,11 @@ "line": 38, "ignorable": true }, + { + "message": "Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.", + "line": 38, + "ignorable": true + }, { "message": "If condition is always true.", "line": 47, @@ -35,20 +40,40 @@ "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", + "line": 74, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 74, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", "line": 79, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Unused result of ternary operator.", + "line": 79, + "ignorable": true + }, + { + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 84, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 84, "ignorable": true }, + { + "message": "Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.", + "line": 89, + "ignorable": true + }, { "message": "Ternary operator condition is always true.", "line": 89, @@ -59,6 +84,11 @@ "line": 94, "ignorable": true }, + { + "message": "Unused result of ternary operator.", + "line": 94, + "ignorable": true + }, { "message": "Left side of && is always true.", "line": 102, @@ -68,5 +98,10 @@ "message": "Right side of && is always true.", "line": 102, "ignorable": true + }, + { + "message": "Unused result of ternary operator.", + "line": 102, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json deleted file mode 100644 index b285fc696d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", - "line": 18, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", - "line": 27, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", - "line": 36, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", - "line": 45, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", - "line": 54, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", - "line": 71, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", - "line": 77, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", - "line": 82, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", - "line": 87, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", - "line": 92, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", - "line": 97, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/variables-10.json b/tests/PHPStan/Levels/data/variables-10.json new file mode 100644 index 0000000000..fd397067b9 --- /dev/null +++ b/tests/PHPStan/Levels/data/variables-10.json @@ -0,0 +1,7 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 7, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/dynamicConstantNames.neon b/tests/PHPStan/Levels/dynamicConstantNames.neon index 89f4491bc4..f52cabb14e 100644 --- a/tests/PHPStan/Levels/dynamicConstantNames.neon +++ b/tests/PHPStan/Levels/dynamicConstantNames.neon @@ -4,3 +4,4 @@ includes: parameters: dynamicConstantNames: - Levels\Comparison\Foo::FOO_CONST + phpVersion: 80300 diff --git a/tests/PHPStan/Levels/namedArguments.neon b/tests/PHPStan/Levels/namedArguments.neon new file mode 100644 index 0000000000..3bc9994f11 --- /dev/null +++ b/tests/PHPStan/Levels/namedArguments.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80000 diff --git a/tests/PHPStan/Levels/staticReflection.neon b/tests/PHPStan/Levels/staticReflection.neon deleted file mode 100644 index 8f93e584da..0000000000 --- a/tests/PHPStan/Levels/staticReflection.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - phpVersion: 80000 - featureToggles: - disableRuntimeReflectionProvider: true diff --git a/tests/PHPStan/Node/AttributeArgRule.php b/tests/PHPStan/Node/AttributeArgRule.php new file mode 100644 index 0000000000..084671311f --- /dev/null +++ b/tests/PHPStan/Node/AttributeArgRule.php @@ -0,0 +1,32 @@ + + */ +class AttributeArgRule implements Rule +{ + + public const ERROR_MESSAGE = 'Found Arg'; + + public function getNodeType(): string + { + return Node\Arg::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attributeArg') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/AttributeArgRuleTest.php b/tests/PHPStan/Node/AttributeArgRuleTest.php new file mode 100644 index 0000000000..796d5d3bdb --- /dev/null +++ b/tests/PHPStan/Node/AttributeArgRuleTest.php @@ -0,0 +1,58 @@ + + */ +class AttributeArgRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeArgRule(); + } + + public static 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); + } + + #[RequiresPhp('>= 8.1')] + public function testEnumCaseAttribute(): void + { + $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..67141a33b2 --- /dev/null +++ b/tests/PHPStan/Node/AttributeGroupRuleTest.php @@ -0,0 +1,46 @@ + + */ +class AttributeGroupRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeGroupRule(); + } + + public static 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..135a0ca20d --- /dev/null +++ b/tests/PHPStan/Node/AttributeRuleTest.php @@ -0,0 +1,46 @@ + + */ +class AttributeRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeRule(); + } + + public static 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..aebcd787be 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -5,9 +5,14 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function get_class; +use function sprintf; +use const DIRECTORY_SEPARATOR; class FileNodeTest extends RuleTestCase { @@ -22,9 +27,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,21 +36,23 @@ 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(), ]; } }; } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [ __DIR__ . '/data/empty.php', @@ -67,12 +73,7 @@ public function dataRule(): iterable ]; } - /** - * @dataProvider dataRule - * @param string $file - * @param string $expectedError - * @param int $line - */ + #[DataProvider('dataRule')] public function testRule(string $file, string $expectedError, int $line): void { $this->analyse([$file], [ diff --git a/tests/PHPStan/Node/ParentStmtTypesRule.php b/tests/PHPStan/Node/ParentStmtTypesRule.php new file mode 100644 index 0000000000..d011615773 --- /dev/null +++ b/tests/PHPStan/Node/ParentStmtTypesRule.php @@ -0,0 +1,34 @@ + + */ +class ParentStmtTypesRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Echo_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(sprintf( + 'Parents: %s', + implode(', ', array_reverse($node->getAttribute('parentStmtTypes'))), + ))->identifier('tests.parentStmtTypes')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/ParentStmtTypesRuleTest.php b/tests/PHPStan/Node/ParentStmtTypesRuleTest.php new file mode 100644 index 0000000000..320a26465f --- /dev/null +++ b/tests/PHPStan/Node/ParentStmtTypesRuleTest.php @@ -0,0 +1,29 @@ + + */ +class ParentStmtTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParentStmtTypesRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parent-stmt-types.php'], [ + [ + 'Parents: PhpParser\Node\Stmt\If_, PhpParser\Node\Stmt\Function_, PhpParser\Node\Stmt\Namespace_', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Node/TryCatchTypeRule.php b/tests/PHPStan/Node/TryCatchTypeRule.php new file mode 100644 index 0000000000..e71958eedf --- /dev/null +++ b/tests/PHPStan/Node/TryCatchTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TryCatchTypeRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Echo_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $tryCatchTypes = $node->getAttribute('tryCatchTypes'); + $type = null; + if ($tryCatchTypes !== null) { + $type = TypeCombinator::union(...array_map(static fn (string $name) => new ObjectType($name), $tryCatchTypes)); + } + return [ + RuleErrorBuilder::message(sprintf( + 'Try catch type: %s', + $type !== null ? $type->describe(VerbosityLevel::precise()) : 'nothing', + ))->identifier('tests.tryCatchType')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/TryCatchTypeRuleTest.php b/tests/PHPStan/Node/TryCatchTypeRuleTest.php new file mode 100644 index 0000000000..e1e649fc1d --- /dev/null +++ b/tests/PHPStan/Node/TryCatchTypeRuleTest.php @@ -0,0 +1,45 @@ + + */ +class TryCatchTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TryCatchTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/try-catch-type.php'], [ + [ + 'Try catch type: nothing', + 10, + ], + [ + 'Try catch type: LogicException|RuntimeException', + 12, + ], + [ + 'Try catch type: nothing', + 14, + ], + [ + 'Try catch type: LogicException|RuntimeException|TypeError', + 17, + ], + [ + 'Try catch type: LogicException|RuntimeException', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Node/data/attributes.php b/tests/PHPStan/Node/data/attributes.php new file mode 100644 index 0000000000..0103b343c4 --- /dev/null +++ b/tests/PHPStan/Node/data/attributes.php @@ -0,0 +1,41 @@ += 8.0 + +namespace NodeCallbackCalled; + +use ClassAttributes\AttributeWithConstructor; +use FunctionAttributes\Baz; + +#[\Attribute(flags: \Attribute::TARGET_ALL)] +class UniversalAttribute +{ + public function __construct(int $foo) + { + } +} + +#[UniversalAttribute(1)] +class MyClass +{ + + #[UniversalAttribute(2)] + private const MY_CONST = 'const'; + + #[UniversalAttribute(3)] + private string $myProperty; + + #[UniversalAttribute(4)] + public function myMethod(#[UniversalAttribute(5)] string $arg): void + { + + } + +} + +#[UniversalAttribute(6)] +interface MyInterface {} + +#[UniversalAttribute(7)] +trait MyTrait {} + +#[UniversalAttribute(8)] +function myFunction() {} diff --git a/tests/PHPStan/Node/data/enum-case-attribute.php b/tests/PHPStan/Node/data/enum-case-attribute.php new file mode 100644 index 0000000000..93e87ef1f6 --- /dev/null +++ b/tests/PHPStan/Node/data/enum-case-attribute.php @@ -0,0 +1,13 @@ += 8.1 + +namespace EnumCaseAttributeCheck; + +use NodeCallbackCalled\UniversalAttribute; + +enum Foo +{ + + #[UniversalAttribute(1)] + case TEST; + +} diff --git a/tests/PHPStan/Rules/data/node-connecting.php b/tests/PHPStan/Node/data/parent-stmt-types.php similarity index 100% rename from tests/PHPStan/Rules/data/node-connecting.php rename to tests/PHPStan/Node/data/parent-stmt-types.php diff --git a/tests/PHPStan/Node/data/try-catch-type.php b/tests/PHPStan/Node/data/try-catch-type.php new file mode 100644 index 0000000000..150cdc95f7 --- /dev/null +++ b/tests/PHPStan/Node/data/try-catch-type.php @@ -0,0 +1,29 @@ + 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([ @@ -61,6 +72,7 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], ], ], @@ -71,16 +83,21 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 10, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 15, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], ], ], diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index 538dd69016..284af8d55c 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -2,12 +2,16 @@ namespace PHPStan\Parallel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use function array_fill; +use function array_map; +use function count; class SchedulerTest extends TestCase { - public function dataSchedule(): array + public static function dataSchedule(): array { return [ [ @@ -68,15 +72,13 @@ public function dataSchedule(): array } /** - * @dataProvider dataSchedule - * @param int $cpuCores - * @param int $maximumNumberOfProcesses - * @param int $minimumNumberOfJobsPerProcess - * @param int $jobSize + * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess * @param 0|positive-int $numberOfFiles - * @param int $expectedNumberOfProcesses * @param array $expectedJobSizes */ + #[DataProvider('dataSchedule')] public function testSchedule( int $cpuCores, int $maximumNumberOfProcesses, @@ -84,7 +86,7 @@ public function testSchedule( int $jobSize, int $numberOfFiles, int $expectedNumberOfProcesses, - array $expectedJobSizes + array $expectedJobSizes, ): void { $files = array_fill(0, $numberOfFiles, 'file.php'); @@ -92,9 +94,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/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 3624cc798d..a0df7a1b2a 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,31 +2,32 @@ namespace PHPStan\Parser; +use Generator; +use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; +use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; class CachedParserTest extends PHPStanTestCase { - /** - * @dataProvider dataParseFileClearCache - * @param int $cachedNodesByStringCountMax - * @param int $cachedNodesByStringCountExpected - */ + #[DataProvider('dataParseFileClearCache')] public function testParseFileClearCache( int $cachedNodesByStringCountMax, - int $cachedNodesByStringCountExpected + int $cachedNodesByStringCountExpected, ): void { $parser = new CachedParser( $this->getParserMock(), - $cachedNodesByStringCountMax + $cachedNodesByStringCountMax, ); - $this->assertEquals( + $this->assertSame( $cachedNodesByStringCountMax, - $parser->getCachedNodesByStringCountMax() + $parser->getCachedNodesByStringCountMax(), ); // Add strings to cache @@ -34,18 +35,21 @@ public function testParseFileClearCache( $parser->parseString('string' . $i); } - $this->assertEquals( + $this->assertSame( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByStringCount() + $parser->getCachedNodesByStringCount(), ); $this->assertCount( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByString() + $parser->getCachedNodesByString(), ); } - public function dataParseFileClearCache(): \Generator + /** + * @return Generator + */ + public static function dataParseFileClearCache(): Generator { yield 'even' => [ 'cachedNodesByStringCountMax' => 50, @@ -58,10 +62,7 @@ public function dataParseFileClearCache(): \Generator ]; } - /** - * @return Parser&\PHPUnit\Framework\MockObject\MockObject - */ - private function getParserMock(): Parser + private function getParserMock(): Parser&MockObject { $mock = $this->createMock(Parser::class); @@ -71,30 +72,55 @@ 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 { - $parser = new CachedParser(self::getContainer()->getService('pathRoutingParser'), 500); - $path = __DIR__ . '/data/test.php'; + $fileHelper = self::getContainer()->getByType(FileHelper::class); + $pathRoutingParser = new PathRoutingParser( + $fileHelper, + self::getContainer()->getService('currentPhpVersionRichParser'), + self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), + self::getContainer()->getService('php8Parser'), + null, + ); + $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->assertNull($stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertNull($stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseFile($path); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); } } diff --git a/tests/PHPStan/Parser/CleaningParserTest.php b/tests/PHPStan/Parser/CleaningParserTest.php new file mode 100644 index 0000000000..c94558ed89 --- /dev/null +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -0,0 +1,85 @@ +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..7dd2e22a05 --- /dev/null +++ b/tests/PHPStan/Parser/ParserTest.php @@ -0,0 +1,96 @@ + 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, + ], + ], + ]; + } + + /** + * @param array|array> $expectedVariadics + * @throws ParserErrorsException + */ + #[DataProvider('dataVariadicCallLikes')] + 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]); + } + } + + /** + * @param array|array> $expectedVariadics + * @throws ParserErrorsException + */ + #[DataProvider('dataVariadicCallLikes')] + 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..a821dbdb5c --- /dev/null +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -0,0 +1,547 @@ + 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'], + ], + ]; + } + + /** + * @param array|null> $expectedLines + */ + #[DataProvider('dataLinesToIgnore')] + 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 static 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'], + ], + ]; + } + + /** + * @param array> $expectedErrors + */ + #[DataProvider('dataLinesToIgnoreParseErrors')] + 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 index a6bee51214..b7ee628d37 100644 --- a/tests/PHPStan/Parser/data/test.php +++ b/tests/PHPStan/Parser/data/test.php @@ -2,7 +2,4 @@ namespace CachedParserBug; -class Foo -{ - -} +$a = new class () {}; $b = new class () {}; diff --git a/tests/PHPStan/Parser/data/variadic-functions.php b/tests/PHPStan/Parser/data/variadic-functions.php new file mode 100644 index 0000000000..d1a572e1e0 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-functions.php @@ -0,0 +1,31 @@ += 8.1 + +namespace VariadicMethodEnum; + +enum X { + + function non_variadic_fn1($v) { + } + + function variadic_fn1(...$v) { + } + + function implicit_variadic_fn1() { + $args = func_get_args(); + } +} diff --git a/tests/PHPStan/Parser/data/variadic-methods.php b/tests/PHPStan/Parser/data/variadic-methods.php new file mode 100644 index 0000000000..da6135b967 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-methods.php @@ -0,0 +1,68 @@ +assertSame( + $expected->describe(), + $phpVersions->producesWarningForFinalPrivateMethods()->describe(), + ); + } + + public static 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..90635f3e69 --- /dev/null +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -0,0 +1,55 @@ +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..e7147bb077 --- /dev/null +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -0,0 +1,95 @@ +', new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()])]; + yield ['class-string&literal-string', new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()])]; + yield ['class-string&literal-string', new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()])]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType()); + yield ['array{foo: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + yield ['array{foo?: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + $builder->setOffsetValueType(new ConstantStringType('bar'), new StringType()); + yield ['array{foo?: int, bar: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType()); + yield ['array{int, string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType(), true); + yield ['array{0: int, 1?: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('\'foo\''), new IntegerType()); + yield ['array{"\'foo\'": int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('"foo"'), new IntegerType()); + yield ['array{\'"foo"\': int}', $builder->getArray()]; + } + + #[DataProvider('dataTest')] + public function testParsingDesiredTypeDescription(string $description, Type $expectedType): void + { + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertTrue($expectedType->equals($type), sprintf('Parsing %s did not result in %s, but in %s', $description, $expectedType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))); + + $newDescription = $type->describe(VerbosityLevel::value()); + $newType = $typeStringResolver->resolve($newDescription); + $this->assertTrue($type->equals($newType), sprintf('Parsing %s again did not result in %s, but in %s', $newDescription, $type->describe(VerbosityLevel::value()), $newType->describe(VerbosityLevel::value()))); + } + + #[DataProvider('dataTest')] + public function testDesiredTypeDescription(string $description, Type $expectedType): void + { + $this->assertSame($description, $expectedType->describe(VerbosityLevel::value())); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertSame($description, $type->describe(VerbosityLevel::value())); + } + +} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php b/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php deleted file mode 100644 index 098f2ee4e9..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php +++ /dev/null @@ -1,24 +0,0 @@ -messages; - } - - public function log(string $message): void - { - $this->messages[] = $message; - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php b/tests/PHPStan/Process/Runnable/RunnableQueueTest.php deleted file mode 100644 index 06aad9aceb..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php +++ /dev/null @@ -1,163 +0,0 @@ -queue($one, 1); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(1, $queue->getRunningSize()); - $one->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - $this->assertSame([ - 'Queue not full - looking at first item in the queue', - 'Removing top item from queue - new size is 1', - 'Running process 1', - 'Process 1 finished successfully', - 'Queue empty', - ], $logger->getMessages()); - } - - public function testComplexScenario(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $four = new RunnableStub('4'); - $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $queue->queue($two, 2); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($three, 3); - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($four, 4); - $this->assertSame(7, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - - $one->finish(); - $this->assertSame(4, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $two->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(7, $queue->getRunningSize()); - - $three->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $four->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 6', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 8 => 'Queue not full - looking at first item in the queue', - 9 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 10 => 'Process 1 finished successfully', - 11 => 'Queue not full - looking at first item in the queue', - 12 => 'Removing top item from queue - new size is 5', - 13 => 'Running process 3', - 14 => 'Process 2 finished successfully', - 15 => 'Queue not full - looking at first item in the queue', - 16 => 'Removing top item from queue - new size is 7', - 17 => 'Running process 4', - 18 => 'Process 3 finished successfully', - 19 => 'Queue empty', - 20 => 'Process 4 finished successfully', - 21 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancel(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - $one = new RunnableStub('1'); - $promise = $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $promise->then(static function () use ($logger): void { - $logger->log('Should not happen'); - }, static function (\Exception $e) use ($logger): void { - $logger->log(sprintf('Else callback in test called: %s', $e->getMessage())); - }); - $promise->cancel(); - - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 4 => 'Else callback in test called: Runnable 1 canceled', - 5 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancelAll(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 6); - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $queue->queue($one, 3); - $queue->queue($two, 2); - $queue->queue($three, 3); - - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $queue->cancelAll(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 3', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 5', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 5, new size would be 8', - 8 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 9 => 'Queue not full - looking at first item in the queue', - 10 => 'Removing top item from queue - new size is 5', - 11 => 'Running process 3', - 12 => 'Process 3 finished unsuccessfully: Runnable 3 canceled', - 13 => 'Queue empty', - 14 => 'Process 2 finished unsuccessfully: Runnable 2 canceled', - 15 => 'Queue empty', - ], $logger->getMessages()); - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableStub.php b/tests/PHPStan/Process/Runnable/RunnableStub.php deleted file mode 100644 index d240b3de33..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableStub.php +++ /dev/null @@ -1,44 +0,0 @@ -name = $name; - $this->deferred = new Deferred(); - } - - public function getName(): string - { - return $this->name; - } - - public function finish(): void - { - $this->deferred->resolve(); - } - - public function run(): CancellablePromiseInterface - { - /** @var CancellablePromiseInterface */ - return $this->deferred->promise(); - } - - public function cancel(): void - { - $this->deferred->reject(new \PHPStan\Process\Runnable\RunnableCanceledException(sprintf('Runnable %s canceled', $this->getName()))); - } - -} diff --git a/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php new file mode 100644 index 0000000000..5ca035e52c --- /dev/null +++ b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php @@ -0,0 +1,37 @@ +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 345712db1e..2be55e8284 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -2,20 +2,29 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsMethods\Bar; +use AnnotationsMethods\Baz; +use AnnotationsMethods\BazBaz; +use AnnotationsMethods\Foo; +use AnnotationsMethods\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_merge; +use function count; +use function sprintf; -class AnnotationsMethodsClassReflectionExtensionTest extends \PHPStan\Testing\PHPStanTestCase +class AnnotationsMethodsClassReflectionExtensionTest extends PHPStanTestCase { - public function dataMethods(): array + public static function dataMethods(): array { $fooMethods = [ 'getInteger' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -37,7 +46,7 @@ public function dataMethods(): array ], ], 'doSomething' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -59,21 +68,21 @@ public function dataMethods(): array ], ], 'getFooOrBar' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnType' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -95,7 +104,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -117,21 +126,21 @@ public function dataMethods(): array ], ], 'getFooOrBarStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -153,7 +162,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -175,21 +184,21 @@ public function dataMethods(): array ], ], 'getFooOrBarWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -211,7 +220,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -233,154 +242,154 @@ public function dataMethods(): array ], ], 'getFooOrBarStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClass' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'float|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromInterface' => [ - 'class' => \AnnotationsMethods\FooInterface::class, - 'returnType' => \AnnotationsMethods\FooInterface::class, + 'class' => FooInterface::class, + 'returnType' => FooInterface::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'publish' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'Aws\Result', 'isStatic' => false, 'isVariadic' => false, @@ -395,7 +404,7 @@ public function dataMethods(): array ], ], 'rotate' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Image', 'isStatic' => false, 'isVariadic' => false, @@ -417,15 +426,15 @@ public function dataMethods(): array ], ], 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], @@ -435,33 +444,33 @@ public function dataMethods(): array $fooMethods, [ 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'conflictingMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => 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, @@ -483,7 +492,7 @@ public function dataMethods(): array ], ], 'getIpsum' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -498,7 +507,7 @@ public function dataMethods(): array ], ], 'getIpsumStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -513,7 +522,7 @@ public function dataMethods(): array ], ], 'getIpsumWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -528,7 +537,7 @@ public function dataMethods(): array ], ], 'getIpsumStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -543,7 +552,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -565,7 +574,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -587,7 +596,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -609,75 +618,75 @@ public function dataMethods(): array ], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromTrait' => [ - 'class' => \AnnotationsMethods\Baz::class, - 'returnType' => \AnnotationsMethods\BazBaz::class, + 'class' => Baz::class, + 'returnType' => BazBaz::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], - ] + ], ); $bazBazMethods = array_merge( $bazMethods, [ 'getTest' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStatically' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getTestWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithSpecificScalarParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -713,7 +722,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificScalarParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -749,7 +758,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -785,7 +794,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -821,7 +830,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -836,7 +845,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -851,7 +860,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -866,7 +875,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -881,7 +890,7 @@ public function dataMethods(): array ], ], 'doSomethingWithComplicatedParameters' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -917,7 +926,7 @@ public function dataMethods(): array ], ], 'paramMultipleTypesWithExtraSpaces' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'float|int', 'isStatic' => false, 'isVariadic' => false, @@ -938,25 +947,24 @@ 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 */ + #[DataProvider('dataMethods')] public function testMethods(string $className, array $methods): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); @@ -966,51 +974,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(), ); } } @@ -1018,8 +1026,8 @@ public function testMethods(string $className, array $methods): void public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeMethod(): void { - $reflectionProvider = $this->createReflectionProvider(); - $class = $reflectionProvider->getClass(\AnnotationsMethods\Bar::class); + $reflectionProvider = self::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 17e6ffd5e2..f654fdbb51 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -2,204 +2,268 @@ 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\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class AnnotationsPropertiesClassReflectionExtensionTest extends \PHPStan\Testing\PHPStanTestCase +class AnnotationsPropertiesClassReflectionExtensionTest extends PHPStanTestCase { - public function dataProperties(): array + public static 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, ], @@ -209,52 +273,58 @@ public function dataProperties(): array } /** - * @dataProvider dataProperties - * @param string $className * @param array $properties */ + #[DataProvider('dataProperties')] public function testProperties(string $className, array $properties): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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) + $class->hasInstanceProperty($propertyName), + sprintf('Class %s does not define property %s.', $className, $propertyName), ); - $property = $class->getProperty($propertyName, $scope); + $property = $class->getInstanceProperty($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 { - $reflectionProvider = $this->createReflectionProvider(); - $class = $reflectionProvider->getClass(\AnnotationsProperties\Bar::class); + $reflectionProvider = self::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 ea25a9d06f..936d56b4aa 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,18 +2,33 @@ namespace PHPStan\Reflection\Annotations; +use DeprecatedAnnotations\Baz; +use DeprecatedAnnotations\BazInterface; +use DeprecatedAnnotations\DeprecatedBar; +use DeprecatedAnnotations\DeprecatedFoo; +use DeprecatedAnnotations\DeprecatedWithMultipleTags; +use DeprecatedAnnotations\Foo; +use DeprecatedAnnotations\FooInterface; +use DeprecatedAnnotations\SubBazInterface; +use DeprecatedAttributeConstants\FooWithConstants; +use DeprecatedAttributeMethods\FooWithMethods; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; -class DeprecatedAnnotationsTest extends \PHPStan\Testing\PHPStanTestCase +class DeprecatedAnnotationsTest extends PHPStanTestCase { - public function dataDeprecatedAnnotations(): array + public static function dataDeprecatedAnnotations(): array { return [ [ false, - \DeprecatedAnnotations\Foo::class, + Foo::class, null, [ 'constant' => [ @@ -25,13 +40,15 @@ public function dataDeprecatedAnnotations(): array ], 'property' => [ 'foo' => null, + ], + 'staticProperty' => [ 'staticFoo' => null, ], ], ], [ true, - \DeprecatedAnnotations\DeprecatedFoo::class, + DeprecatedFoo::class, 'in 1.0.0.', [ 'constant' => [ @@ -43,13 +60,15 @@ public function dataDeprecatedAnnotations(): array ], 'property' => [ 'deprecatedFoo' => null, + ], + 'staticProperty' => [ 'deprecatedStaticFoo' => null, ], ], ], [ false, - \DeprecatedAnnotations\FooInterface::class, + FooInterface::class, null, [ 'constant' => [ @@ -63,7 +82,7 @@ public function dataDeprecatedAnnotations(): array ], [ true, - \DeprecatedAnnotations\DeprecatedWithMultipleTags::class, + DeprecatedWithMultipleTags::class, "in Foo 1.1.0 and will be removed in 1.5.0, use\n \\Foo\\Bar\\NotDeprecated instead.", [ 'method' => [ @@ -75,20 +94,19 @@ public function dataDeprecatedAnnotations(): array } /** - * @dataProvider dataDeprecatedAnnotations - * @param bool $deprecated - * @param string $className - * @param string|null $classDeprecation * @param array $deprecatedAnnotations */ + #[DataProvider('dataDeprecatedAnnotations')] public function testDeprecatedAnnotations(bool $deprecated, string $className, ?string $classDeprecation, array $deprecatedAnnotations): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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()); @@ -100,7 +118,13 @@ public function testDeprecatedAnnotations(bool $deprecated, string $className, ? } foreach ($deprecatedAnnotations['property'] ?? [] as $propertyName => $deprecatedMessage) { - $propertyAnnotation = $class->getProperty($propertyName, $scope); + $propertyAnnotation = $class->getInstanceProperty($propertyName, $scope); + $this->assertSame($deprecated, $propertyAnnotation->isDeprecated()->yes()); + $this->assertSame($deprecatedMessage, $propertyAnnotation->getDeprecatedDescription()); + } + + foreach ($deprecatedAnnotations['staticProperty'] ?? [] as $propertyName => $deprecatedMessage) { + $propertyAnnotation = $class->getStaticProperty($propertyName); $this->assertSame($deprecated, $propertyAnnotation->isDeprecated()->yes()); $this->assertSame($deprecatedMessage, $propertyAnnotation->getDeprecatedDescription()); } @@ -116,7 +140,7 @@ public function testDeprecatedUserFunctions(): void { require_once __DIR__ . '/data/annotations-deprecated.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\foo'), null)->isDeprecated()->yes()); $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\deprecatedFoo'), null)->isDeprecated()->yes()); @@ -124,11 +148,256 @@ public function testDeprecatedUserFunctions(): void public function testNonDeprecatedNativeFunctions(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $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 = self::createReflectionProvider(); + $class = $reflectionProvider->getClass(DeprecatedBar::class); + $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); + } + + public function testNotDeprecatedChildMethods(): void + { + $reflectionProvider = self::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 static 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', + ]; + } + + /** + * @param non-empty-string $functionName + */ + #[DataProvider('dataDeprecatedAttributeAboveFunction')] + public function testDeprecatedAttributeAboveFunction(string $functionName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + require_once __DIR__ . '/data/deprecated-attribute-functions.php'; + + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $this->assertSame($isDeprecated->describe(), $function->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $function->getDeprecatedDescription()); + } + + public static 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 = self::createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $method = $class->getNativeMethod($methodName); + $this->assertSame($isDeprecated->describe(), $method->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $method->getDeprecatedDescription()); + } + + public static 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 = self::createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $constant = $class->getConstant($constantName); + $this->assertSame($isDeprecated->describe(), $constant->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $constant->getDeprecatedDescription()); + } + + public static function dataDeprecatedAttributeAboveEnumCase(): iterable + { + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + } + + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataDeprecatedAttributeAboveEnumCase')] + public function testDeprecatedAttributeAboveEnumCase(string $className, string $caseName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $case = $class->getEnumCase($caseName); + $this->assertSame($isDeprecated->describe(), $case->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $case->getDeprecatedDescription()); + } + + public static 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', + ]; + } + + /** + * @param 'get'|'set' $hookName + */ + #[RequiresPhp('>= 8.4')] + #[DataProvider('dataDeprecatedAttributeAbovePropertyHook')] + public function testDeprecatedAttributeAbovePropertyHook(string $className, string $propertyName, string $hookName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + $reflectionProvider = self::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..aef982a7d7 --- /dev/null +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAttributePhpFunctionFromParserReflectionRuleTest.php @@ -0,0 +1,142 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHookRule(): void + { + $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 8ac8a769b7..b24c82fba9 100644 --- a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php @@ -2,18 +2,21 @@ namespace PHPStan\Reflection\Annotations; -use PhpParser\Node\Name; +use FinalAnnotations\FinalFoo; +use FinalAnnotations\Foo; use PHPStan\Analyser\Scope; +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class FinalAnnotationsTest extends \PHPStan\Testing\PHPStanTestCase +class FinalAnnotationsTest extends PHPStanTestCase { - public function dataFinalAnnotations(): array + public static function dataFinalAnnotations(): array { return [ [ false, - \FinalAnnotations\Foo::class, + Foo::class, [ 'method' => [ 'foo', @@ -23,7 +26,7 @@ public function dataFinalAnnotations(): array ], [ true, - \FinalAnnotations\FinalFoo::class, + FinalFoo::class, [ 'method' => [ 'finalFoo', @@ -35,19 +38,19 @@ public function dataFinalAnnotations(): array } /** - * @dataProvider dataFinalAnnotations - * @param bool $final - * @param string $className * @param array $finalAnnotations */ + #[DataProvider('dataFinalAnnotations')] public function testFinalAnnotations(bool $final, string $className, array $finalAnnotations): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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()); @@ -57,14 +60,4 @@ public function testFinalAnnotations(bool $final, string $className, array $fina } } - public function testFinalUserFunctions(): void - { - require_once __DIR__ . '/data/annotations-final.php'; - - $reflectionProvider = $this->createReflectionProvider(); - - $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('FinalAnnotations\foo'), null)->isFinal()->yes()); - $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('FinalAnnotations\finalFoo'), null)->isFinal()->yes()); - } - } diff --git a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php index be4c411a54..9e4e36901f 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -2,18 +2,26 @@ 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\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class InternalAnnotationsTest extends \PHPStan\Testing\PHPStanTestCase +class InternalAnnotationsTest extends PHPStanTestCase { - public function dataInternalAnnotations(): array + public static function dataInternalAnnotations(): array { return [ [ false, - \InternalAnnotations\Foo::class, + Foo::class, [ 'constant' => [ 'FOO', @@ -24,13 +32,15 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], ], [ true, - \InternalAnnotations\InternalFoo::class, + InternalFoo::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -41,13 +51,15 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], ], [ false, - \InternalAnnotations\FooInterface::class, + FooInterface::class, [ 'constant' => [ 'FOO', @@ -60,7 +72,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFooInterface::class, + InternalFooInterface::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -73,7 +85,7 @@ public function dataInternalAnnotations(): array ], [ false, - \InternalAnnotations\FooTrait::class, + FooTrait::class, [ 'method' => [ 'foo', @@ -81,13 +93,15 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], ], [ true, - \InternalAnnotations\InternalFooTrait::class, + InternalFooTrait::class, [ 'method' => [ 'internalFoo', @@ -95,6 +109,8 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], @@ -103,19 +119,19 @@ public function dataInternalAnnotations(): array } /** - * @dataProvider dataInternalAnnotations - * @param bool $internal - * @param string $className * @param array $internalAnnotations */ + #[DataProvider('dataInternalAnnotations')] public function testInternalAnnotations(bool $internal, string $className, array $internalAnnotations): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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()); @@ -125,7 +141,12 @@ public function testInternalAnnotations(bool $internal, string $className, array } foreach ($internalAnnotations['property'] ?? [] as $propertyName) { - $propertyAnnotation = $class->getProperty($propertyName, $scope); + $propertyAnnotation = $class->getInstanceProperty($propertyName, $scope); + $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); + } + + foreach ($internalAnnotations['staticProperty'] ?? [] as $propertyName) { + $propertyAnnotation = $class->getStaticProperty($propertyName); $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); } @@ -139,7 +160,7 @@ public function testInternalUserFunctions(): void { require_once __DIR__ . '/data/annotations-internal.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $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 4dc0fc8aa3..b58322ca01 100644 --- a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php @@ -4,56 +4,64 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; - -class ThrowsAnnotationsTest extends \PHPStan\Testing\PHPStanTestCase +use PHPUnit\Framework\Attributes\DataProvider; +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 + public static function dataThrowsAnnotations(): array { return [ [ - \ThrowsAnnotations\Foo::class, + Foo::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\PhpstanFoo::class, + PhpstanFoo::class, [ 'withoutThrows' => 'void', - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooInterface::class, + FooInterface::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooTrait::class, + FooTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\BarTrait::class, + BarTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], @@ -61,13 +69,12 @@ public function dataThrowsAnnotations(): array } /** - * @dataProvider dataThrowsAnnotations - * @param string $className * @param array $throwsAnnotations */ + #[DataProvider('dataThrowsAnnotations')] public function testThrowsAnnotations(string $className, array $throwsAnnotations): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); @@ -82,13 +89,13 @@ public function testThrowsOnUserFunctions(): void { require_once __DIR__ . '/data/annotations-throws.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertNull($reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\withoutThrows'), null)->getThrowType()); $throwType = $reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\throwsRuntime'), null)->getThrowType(); $this->assertNotNull($throwType); - $this->assertSame(\RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); + $this->assertSame(RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); } } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 0d4db04b06..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/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..a07cbead1b --- /dev/null +++ b/tests/PHPStan/Reflection/AttributeReflectionFromNodeRuleTest.php @@ -0,0 +1,110 @@ +> + */ +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(), + ]; + } + + }; + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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..2dda376cc0 --- /dev/null +++ b/tests/PHPStan/Reflection/AttributeReflectionTest.php @@ -0,0 +1,178 @@ +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', + ], + ], + ], + ]; + } + + /** + * @param list $attributeReflections + * @param list}> $expectations + */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataAttributeReflections')] + public function testAttributeReflections( + array $attributeReflections, + array $expectations, + ): void + { + $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 4196986d48..6e4c91a783 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -3,16 +3,18 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PHPStan\BetterReflection\Reflection\ReflectionClass; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\DefaultReflector; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Constant\ConstantIntegerType; use TestSingleFileSourceLocator\AFoo; use TestSingleFileSourceLocator\InCondition; +use function class_alias; function testFunctionForLocator(): void // phpcs:disable { - + echo 'test'; } class AutoloadSourceLocatorTest extends PHPStanTestCase @@ -20,36 +22,44 @@ 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 84e3d0646a..06c9407908 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php @@ -2,18 +2,25 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\BetterReflection\Reflector\ClassReflector; +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\BetterReflection\Reflector\FunctionReflector; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use TestDirectorySourceLocator\AFoo; +use TestDirectorySourceLocator\EmptyClass; +use function array_map; +use function basename; +use const PHP_VERSION_ID; class OptimizedDirectorySourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public static function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -25,35 +32,58 @@ 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 - */ + #[DataProvider('dataClass')] public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $classReflection = $classReflector->reflect($className); + $reflector = new DefaultReflector($locator); + $classReflection = $reflector->reflectClass($className); $this->assertSame($expectedClassName, $classReflection->getName()); $this->assertNotNull($classReflection->getFileName()); $this->assertSame($file, basename($classReflection->getFileName())); } - public function dataFunctionExists(): array + public static function dataFunctionExists(): array { return [ [ @@ -67,52 +97,195 @@ public function dataFunctionExists(): array 'a.php', ], [ - 'doBar', - 'doBar', + 'OptimizedDirectory\\doBar', + 'OptimizedDirectory\\doBar', + 'b.php', + ], + [ + 'OptimizedDirectory\\doBaz', + 'OptimizedDirectory\\doBaz', 'b.php', ], [ - 'doBaz', - 'doBaz', + 'OptimizedDirectory\\dobaz', + 'OptimizedDirectory\\doBaz', 'b.php', ], [ - 'dobaz', - 'doBaz', + 'OptimizedDirectory\\get_smarty', + 'OptimizedDirectory\\get_smarty', 'b.php', ], [ - 'get_smarty', - 'get_smarty', + 'OptimizedDirectory\\get_smarty2', + 'OptimizedDirectory\\get_smarty2', 'b.php', ], [ - 'get_smarty2', - 'get_smarty2', + 'OptimizedDirectory\\upperCaseFunction', + 'OptimizedDirectory\\upperCaseFunction', 'b.php', ], ]; } - /** - * @dataProvider dataFunctionExists - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file - */ + #[DataProvider('dataFunctionExists')] public function testFunctionExists(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $functionReflection = $functionReflector->reflect($functionName); + $reflector = new DefaultReflector($locator); + $functionReflection = $reflector->reflectFunction($functionName); $this->assertSame($expectedFunctionName, $functionReflection->getName()); $this->assertNotNull($functionReflection->getFileName()); $this->assertSame($file, basename($functionReflection->getFileName())); } - public function dataFunctionDoesNotExist(): array + public static 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 static function dataFunctionDoesNotExist(): array { return [ ['doFoo'], @@ -120,32 +293,24 @@ public function dataFunctionDoesNotExist(): array ]; } - /** - * @dataProvider dataFunctionDoesNotExist - * @param string $functionName - */ + #[DataProvider('dataFunctionDoesNotExist')] public function testFunctionDoesNotExist(string $functionName): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); + $reflector = new DefaultReflector($locator); $this->expectException(IdentifierNotFound::class); - $functionReflector->reflect($functionName); + $reflector->reflectFunction($functionName); } public function testBug5525(): void { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('This test needs at least PHP 7.3 because of different PCRE engine'); - } - $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByFiles([__DIR__ . '/data/bug-5525.php']); - $classReflector = new ClassReflector($locator); + $reflector = new DefaultReflector($locator); - $class = $classReflector->reflect('Faker\\Provider\\nl_BE\\Text'); + $class = $reflector->reflectClass('Faker\\Provider\\nl_BE\\Text'); /** @var string $className */ $className = $class->getName(); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index c28ab59464..f8c0875f69 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -2,19 +2,26 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; +use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use SingleFileSourceLocatorTestClass; use TestSingleFileSourceLocator\AFoo; +use function array_map; +use const PHP_VERSION_ID; class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public static function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -26,34 +33,112 @@ public function dataClass(): array __DIR__ . '/data/a.php', ], [ - \SingleFileSourceLocatorTestClass::class, - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], [ 'SinglefilesourceLocatortestClass', - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'OptimizedDirectory\\TestEnum', + 'OptimizedDirectory\\TestEnum', + __DIR__ . '/data/directory/enum.php', + ]; } - /** - * @dataProvider dataClass - * @param string $className - * @param string $expectedClassName - * @param string $file - */ + public static 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')] 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()); } - public function dataFunction(): array + public static function dataFunction(): array { return [ [ @@ -79,79 +164,98 @@ public function dataFunction(): array ]; } - /** - * @dataProvider dataFunction - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file - */ + #[DataProvider('dataFunction')] 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()); } - public function dataConst(): array + public static function dataConst(): array { return [ [ 'ConstFile\\TABLE_NAME', - 'resized_images', + "'resized_images'", ], [ 'ANOTHER_NAME', - 'foo_images', + "'foo_images'", ], [ 'ConstFile\\ANOTHER_NAME', - 'bar_images', + "'bar_images'", ], [ 'const_with_dir_const', - str_replace('\\', '/', __DIR__ . '/data'), + 'literal-string&non-falsy-string', + ], + [ + 'OPTIMIZED_SFSL_OBJECT_CONSTANT', + 'stdClass', ], ]; } - /** - * @dataProvider dataConst - * @param string $constantName - * @param mixed $value - */ - public function testConst(string $constantName, $value): void + #[DataProvider('dataConst')] + 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 + public static function dataConstUnknown(): array { return [ ['TEST_VARIABLE'], ]; } - /** - * @dataProvider dataConstUnknown - * @param string $constantName - */ + #[DataProvider('dataConstUnknown')] 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); + } + + /** + * @param class-string[] $expectedIdentifiers + */ + #[DataProvider('dataForIdenifiersByType')] + 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/const.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/const.php index eba1d99941..ce333cbfb7 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/const.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/const.php @@ -10,3 +10,5 @@ define('TEST_VARIABLE', $foo); define('const_with_dir_const', __DIR__); + +define('OPTIMIZED_SFSL_OBJECT_CONSTANT', new \stdClass()); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php new file mode 100644 index 0000000000..0785f66561 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php @@ -0,0 +1,9 @@ += 8.1 + +namespace TestDirectorySourceLocator\Something; + +class Whatever +{ + const CONSTANT = 'constant'; + + public const PUBLIC_CONSTANT = 'constant'; + + final public const FINAL_PUBLIC_CONSTANT = 'constant'; +} + diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php new file mode 100644 index 0000000000..e1e9307aa2 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php @@ -0,0 +1,11 @@ +define('DEFINE_THAT_SHOULD_SURVIVE_METHOD_CALL', 'no_define'); + } +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php index 47b700b0ea..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 @@ +getReflectors(); - $reflection = $classReflector->reflect(\Throwable::class); - $this->assertSame(\Throwable::class, $reflection->getName()); - } - - public function testFunction(): void - { - /** @var FunctionReflector $functionReflector */ - [, $functionReflector] = $this->getReflectors(); - $reflection = $functionReflector->reflect('htmlspecialchars'); - $this->assertSame('htmlspecialchars', $reflection->getName()); - } - - /** - * @return array{ClassReflector, FunctionReflector} - */ - private function getReflectors(): array - { - // memoizing parser screws things up so we need to create the universe from the start - $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, new Emulative([ - 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], - ])); - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $astLocator = new Locator($parser, static function () use (&$functionReflector): FunctionReflector { - return $functionReflector; - }); - $sourceStubber = new Php8StubsSourceStubber(); - $phpInternalSourceLocator = new PhpInternalSourceLocator( - $astLocator, - $sourceStubber - ); - $classReflector = new ClassReflector($phpInternalSourceLocator); - $functionReflector = new FunctionReflector($phpInternalSourceLocator, $classReflector); - - return [$classReflector, $functionReflector]; - } - -} diff --git a/tests/PHPStan/Reflection/ClassReflectionPropertyHooksTest.php b/tests/PHPStan/Reflection/ClassReflectionPropertyHooksTest.php new file mode 100644 index 0000000000..80c75e06cf --- /dev/null +++ b/tests/PHPStan/Reflection/ClassReflectionPropertyHooksTest.php @@ -0,0 +1,350 @@ += 8.4')] +class ClassReflectionPropertyHooksTest extends PHPStanTestCase +{ + + public static function dataPropertyHooks(): iterable + { + $reflectionProvider = self::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, + ]; + } + + /** + * @param ExtendedPropertyReflection::HOOK_* $hookName + * @param string[] $parameterTypes + */ + #[DataProvider('dataPropertyHooks')] + 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/ClassReflectionTest.php b/tests/PHPStan/Reflection/ClassReflectionTest.php index c900dc65f1..63c527ebd0 100644 --- a/tests/PHPStan/Reflection/ClassReflectionTest.php +++ b/tests/PHPStan/Reflection/ClassReflectionTest.php @@ -2,139 +2,128 @@ namespace PHPStan\Reflection; +use Attribute; use Attributes\IsAttribute; use Attributes\IsAttribute2; use Attributes\IsAttribute3; use Attributes\IsNotAttribute; -use PHPStan\Broker\Broker; -use PHPStan\Php\PhpVersion; -use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Type\FileTypeMapper; +use GenericInheritance\C; +use HasTraitUse\Bar; +use HasTraitUse\Baz; +use HasTraitUse\Foo; +use HasTraitUse\FooTrait; +use HierarchyDistances\ExtendedIpsumInterface; +use HierarchyDistances\FirstIpsumInterface; +use HierarchyDistances\FirstLoremInterface; +use HierarchyDistances\Ipsum; +use HierarchyDistances\Lorem; +use HierarchyDistances\SecondIpsumInterface; +use HierarchyDistances\SecondLoremInterface; +use HierarchyDistances\ThirdIpsumInterface; +use HierarchyDistances\TraitOne; +use HierarchyDistances\TraitThree; +use HierarchyDistances\TraitTwo; +use NestedTraits\BarTrait; +use NestedTraits\BazChild; +use NestedTraits\BazTrait; +use NestedTraits\NoTrait; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\IntegerType; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\TestCase; +use ReflectionClass; use WrongClassConstantFile\SecuredRouter; +use function array_map; +use function array_values; -class ClassReflectionTest extends \PHPStan\Testing\PHPStanTestCase +class ClassReflectionTest extends PHPStanTestCase { - public function dataHasTraitUse(): array + public static function dataHasTraitUse(): array { return [ - [\HasTraitUse\Foo::class, true], - [\HasTraitUse\Bar::class, true], - [\HasTraitUse\Baz::class, false], + [Foo::class, true], + [Bar::class, true], + [Baz::class, false], ]; } /** - * @dataProvider dataHasTraitUse * @param class-string $className - * @param bool $has */ + #[DataProvider('dataHasTraitUse')] public function testHasTraitUse(string $className, bool $has): void { - $broker = $this->createMock(Broker::class); - $fileTypeMapper = $this->createMock(FileTypeMapper::class); - $stubPhpDocProvider = $this->createMock(StubPhpDocProvider::class); - $phpDocInheritanceResolver = $this->createMock(PhpDocInheritanceResolver::class); - $classReflection = new ClassReflection($broker, $fileTypeMapper, $stubPhpDocProvider, $phpDocInheritanceResolver, new PhpVersion(PHP_VERSION_ID), [], [], $className, new \ReflectionClass($className), null, null, null); - $this->assertSame($has, $classReflection->hasTraitUse(\HasTraitUse\FooTrait::class)); + $reflectionProvider = self::createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + $this->assertSame($has, $classReflection->hasTraitUse(FooTrait::class)); } - public function dataClassHierarchyDistances(): array + public static 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, ], ], ]; } /** - * @dataProvider dataClassHierarchyDistances * @param class-string $class * @param int[] $expectedDistances */ + #[DataProvider('dataClassHierarchyDistances')] public function testClassHierarchyDistances( string $class, - array $expectedDistances + array $expectedDistances, ): void { - $broker = $this->createReflectionProvider(); - $fileTypeMapper = $this->createMock(FileTypeMapper::class); - $stubPhpDocProvider = $this->createMock(StubPhpDocProvider::class); - $phpDocInheritanceResolver = $this->createMock(PhpDocInheritanceResolver::class); - - $classReflection = new ClassReflection( - $broker, - $fileTypeMapper, - $stubPhpDocProvider, - $phpDocInheritanceResolver, - new PhpVersion(PHP_VERSION_ID), - [], - [], - $class, - new \ReflectionClass($class), - null, - null, - null - ); + $reflectionProvider = self::createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($class); $this->assertSame( $expectedDistances, - $classReflection->getClassHierarchyDistances() + $classReflection->getClassHierarchyDistances(), ); } public function testVariadicTraitMethod(): void { - $reflectionProvider = $this->createReflectionProvider(); - $fooReflection = $reflectionProvider->getClass(\HasTraitUse\Foo::class); + $reflectionProvider = self::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 { - $reflectionProvider = $this->createReflectionProvider(); - $reflection = $reflectionProvider->getClass(\GenericInheritance\C::class); + $reflectionProvider = self::createReflectionProvider(); + $reflection = $reflectionProvider->getClass(C::class); $this->assertSame('GenericInheritance\\C', $reflection->getDisplayName()); @@ -147,19 +136,17 @@ public function testGenericInheritance(): void 'GenericInheritance\\I', 'GenericInheritance\\I0', 'GenericInheritance\\I1', - ], array_map(static function (ClassReflection $r): string { - return $r->getDisplayName(); - }, array_values($reflection->getInterfaces()))); + ], array_map(static fn (ClassReflection $r): string => $r->getDisplayName(), array_values($reflection->getInterfaces()))); } public function testIsGenericWithStubPhpDoc(): void { - $reflectionProvider = $this->createReflectionProvider(); - $reflection = $reflectionProvider->getClass(\ReflectionClass::class); + $reflectionProvider = self::createReflectionProvider(); + $reflection = $reflectionProvider->getClass(ReflectionClass::class); $this->assertTrue($reflection->isGeneric()); } - public function dataIsAttributeClass(): array + public static function dataIsAttributeClass(): array { return [ [ @@ -173,27 +160,21 @@ public function dataIsAttributeClass(): array [ IsAttribute2::class, true, - \Attribute::IS_REPEATABLE, + Attribute::IS_REPEATABLE, ], [ IsAttribute3::class, true, - \Attribute::IS_REPEATABLE | \Attribute::TARGET_PROPERTY, + Attribute::IS_REPEATABLE | Attribute::TARGET_PROPERTY, ], ]; } - /** - * @dataProvider dataIsAttributeClass - * @param string $className - * @param bool $expected - */ - public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = \Attribute::TARGET_ALL): void + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataIsAttributeClass')] + public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = Attribute::TARGET_ALL): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $this->assertSame($expected, $reflection->isAttributeClass()); if (!$expected) { @@ -204,43 +185,40 @@ public function testIsAttributeClass(string $className, bool $expected, int $exp public function testDeprecatedConstantFromAnotherFile(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass(SecuredRouter::class); $constant = $reflection->getConstant('SECURED'); $this->assertTrue($constant->isDeprecated()->yes()); } /** - * @dataProvider dataNestedRecursiveTraits * @param class-string $className * @param array $expected - * @param bool $recursive */ + #[DataProvider('dataNestedRecursiveTraits')] public function testGetTraits(string $className, array $expected, bool $recursive): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertSame( array_map( - static function (ClassReflection $classReflection): string { - return $classReflection->getNativeReflection()->getName(); - }, - $reflectionProvider->getClass($className)->getTraits($recursive) + static fn (ClassReflection $classReflection): string => $classReflection->getNativeReflection()->getName(), + $reflectionProvider->getClass($className)->getTraits($recursive), ), - $expected + $expected, ); } - public function dataNestedRecursiveTraits(): array + public static function dataNestedRecursiveTraits(): array { return [ [ - \NestedTraits\NoTrait::class, + NoTrait::class, [], false, ], [ - \NestedTraits\NoTrait::class, + NoTrait::class, [], true, ], @@ -261,14 +239,14 @@ public function dataNestedRecursiveTraits(): array [ \NestedTraits\Bar::class, [ - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BarTrait::class => BarTrait::class, ], false, ], [ \NestedTraits\Bar::class, [ - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, @@ -276,29 +254,29 @@ public function dataNestedRecursiveTraits(): array [ \NestedTraits\Baz::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, + BazTrait::class => BazTrait::class, ], false, ], [ \NestedTraits\Baz::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, ], [ - \NestedTraits\BazChild::class, + BazChild::class, [], false, ], [ - \NestedTraits\BazChild::class, + BazChild::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, @@ -306,4 +284,38 @@ public function dataNestedRecursiveTraits(): array ]; } + #[RequiresPhp('>= 8.1')] + public function testEnumIsFinal(): void + { + $reflectionProvider = self::createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertTrue($enum->isEnum()); + + // @phpstan-ignore-next-line Exact error differs on PHP 7.4 and others + $this->assertInstanceOf('ReflectionEnum', $enum->getNativeReflection()); + $this->assertTrue($enum->isFinal()); + $this->assertTrue($enum->isFinalByKeyword()); + } + + #[RequiresPhp('>= 8.1')] + public function testBackedEnumType(): void + { + $reflectionProvider = self::createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType()); + } + + public function testIs(): void + { + $className = static::class; + + $reflectionProvider = self::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)); + } + } diff --git a/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php new file mode 100644 index 0000000000..925cb4350d --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php @@ -0,0 +1,57 @@ += 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 = self::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 @@ += 8.0')] + public function testCustomDeprecations(): void + { + require __DIR__ . '/data/deprecations.php'; + + $reflectionProvider = self::createReflectionProvider(); + + $notDeprecatedClass = $reflectionProvider->getClass(NotDeprecatedClass::class); + $attributeDeprecatedClass = $reflectionProvider->getClass(AttributeDeprecatedClass::class); + + // @phpstan-ignore classConstant.deprecatedClass + $phpDocDeprecatedClass = $reflectionProvider->getClass(PhpDocDeprecatedClass::class); + + // @phpstan-ignore classConstant.deprecatedClass + $phpDocDeprecatedClassWithMessages = $reflectionProvider->getClass(PhpDocDeprecatedClassWithMessage::class); + $attributeDeprecatedClassWithMessages = $reflectionProvider->getClass(AttributeDeprecatedClassWithMessage::class); + + // @phpstan-ignore classConstant.deprecatedClass + $doubleDeprecatedClass = $reflectionProvider->getClass(DoubleDeprecatedClass::class); + + // @phpstan-ignore classConstant.deprecatedClass + $doubleDeprecatedClassOnlyPhpDocMessage = $reflectionProvider->getClass(DoubleDeprecatedClassOnlyPhpDocMessage::class); + + // @phpstan-ignore classConstant.deprecatedClass + $doubleDeprecatedClassOnlyAttributeMessage = $reflectionProvider->getClass(DoubleDeprecatedClassOnlyAttributeMessage::class); + + $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->getInstanceProperty('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getInstanceProperty('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getInstanceProperty('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getInstanceProperty('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getInstanceProperty('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getInstanceProperty('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getInstanceProperty('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()); + } + + #[RequiresPhp('>= 8.1')] + public function testCustomDeprecationsOfEnumCases(): void + { + 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..d1d66b44d9 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecationExtension.php @@ -0,0 +1,88 @@ += 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 +{ + + /** + * @param ReflectionClass|ReflectionEnum $reflection + */ + public function getClassDeprecation($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); + } + + /** + * @param ReflectionEnumBackedCase|ReflectionEnumUnitCase $reflection + */ + public function getEnumCaseDeprecation($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..12dd5ee438 --- /dev/null +++ b/tests/PHPStan/Reflection/FunctionReflectionTest.php @@ -0,0 +1,205 @@ +getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedDoc, $functionReflection->getDocComment()); + } + + public static 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 = self::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 static function dataFunctionReturnsByReference(): iterable + { + yield ['\\implode', TrinaryLogic::createNo()]; + + yield ['ReturnsByReference\\foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\refFoo', TrinaryLogic::createYes()]; + } + + /** + * @param non-empty-string $functionName + */ + #[DataProvider('dataFunctionReturnsByReference')] + public function testFunctionReturnsByReference(string $functionName, TrinaryLogic $expectedReturnsByRef): void + { + require_once __DIR__ . '/data/returns-by-reference.php'; + + $reflectionProvider = self::createReflectionProvider(); + + $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedReturnsByRef, $functionReflection->returnsByReference()); + } + + public static 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 = self::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 7168dfc167..ea627a029f 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,27 +14,28 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function count; +use function get_class; +use function sprintf; -class GenericParametersAcceptorResolverTest extends \PHPStan\Testing\PHPStanTestCase +class GenericParametersAcceptorResolverTest extends PHPStanTestCase { /** * @return array */ - public function dataResolve(): array + public static 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 +54,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -70,11 +72,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'two params, two args, return type' => [ @@ -95,7 +97,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -103,11 +105,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -122,7 +124,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -130,11 +132,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new IntegerType() + new IntegerType(), ), ], 'mixed types' => [ @@ -154,7 +156,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -162,11 +164,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('T') + $templateType('T'), ), new FunctionVariant( new TemplateTypeMap([ @@ -183,7 +185,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -194,14 +196,14 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, new UnionType([ new ObjectType('DateTime'), new IntegerType(), - ]) + ]), ), ], 'parameter default value' => [ @@ -221,7 +223,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -229,11 +231,11 @@ public function dataResolve(): array true, PassedByReference::createNo(), false, - new IntegerType() + new IntegerType(), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -248,7 +250,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -256,11 +258,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'variadic parameter' => [ @@ -283,7 +285,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -291,11 +293,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), true, - null + null, ), ], true, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -310,19 +312,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 +352,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -350,11 +360,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -369,7 +379,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -377,11 +387,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'constant string arg resolved to constant string' => [ @@ -397,41 +407,42 @@ 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'), ), ], ]; } /** - * @dataProvider dataResolve - * @param \PHPStan\Type\Type[] $argTypes + * @param Type[] $argTypes */ + #[DataProvider('dataResolve')] 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 +454,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..6c3ba79cfb --- /dev/null +++ b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php @@ -0,0 +1,130 @@ + new ConstantIntegerType(1), + ConstantIntegerType::class, + ]; + } + + /** + * + * @param class-string $resultClass + * @param callable(Expr): Type $callback + */ + #[DataProvider('dataExplicitNever')] + public function testExplicitNever(Expr $left, Expr $right, callable $callback, string $resultClass, ?bool $resultIsExplicit = null): void + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + + $result = $initializerExprTypeResolver->getPlusType( + $left, + $right, + $callback, + ); + $this->assertInstanceOf($resultClass, $result); + + if (!($result instanceof NeverType)) { + return; + } + + if ($resultIsExplicit === null) { + throw new ShouldNotHappenException(); + } + $this->assertSame($resultIsExplicit, $result->isExplicit()); + } + +} diff --git a/tests/PHPStan/Reflection/MixedTypeTest.php b/tests/PHPStan/Reflection/MixedTypeTest.php index ff7aecd0f2..0d30fc39b8 100644 --- a/tests/PHPStan/Reflection/MixedTypeTest.php +++ b/tests/PHPStan/Reflection/MixedTypeTest.php @@ -6,24 +6,25 @@ use PhpParser\Node\Name; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\MixedType; +use const PHP_VERSION_ID; class MixedTypeTest extends PHPStanTestCase { public function testMixedType(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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 = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $methodVariant = $method->getOnlyVariant(); $methodReturnType = $methodVariant->getReturnType(); $this->assertInstanceOf(MixedType::class, $methodReturnType); $this->assertTrue($methodReturnType->isExplicitMixed()); @@ -33,7 +34,7 @@ public function testMixedType(): void $this->assertTrue($methodParameterType->isExplicitMixed()); $function = $reflectionProvider->getFunction(new Name('NativeMixedType\doFoo'), null); - $functionVariant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + $functionVariant = $function->getOnlyVariant(); $functionReturnType = $functionVariant->getReturnType(); $this->assertInstanceOf(MixedType::class, $functionReturnType); $this->assertTrue($functionReturnType->isExplicitMixed()); diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 7b09586bf2..1926cb56e5 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -2,33 +2,49 @@ 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\Reflection\Php\ExtendedDummyParameter; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; 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\Generic\TemplateTypeVarianceMap; 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 PHPStan\Type\VoidType; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function count; -class ParametersAcceptorSelectorTest extends \PHPStan\Testing\PHPStanTestCase +class ParametersAcceptorSelectorTest extends PHPStanTestCase { - public function dataSelectFromTypes(): \Generator + /** + * @return Generator + */ + public static function dataSelectFromTypes(): Generator { - require_once __DIR__ . '/data/function-definitions.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $arrayRandVariants = $reflectionProvider->getFunction(new Name('array_rand'), null)->getVariants(); yield [ @@ -53,25 +69,65 @@ public function dataSelectFromTypes(): \Generator $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(), ], $datePeriodConstructorVariants, false, - $datePeriodConstructorVariants[0], + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + array_map(static fn ($parameter) => new ExtendedDummyParameter( + $parameter->getName(), + TemplateTypeHelper::resolveToBounds($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $parameter->getPhpDocType(), + $parameter->getOutType(), + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType(), + $parameter->getAttributes(), + ), $datePeriodConstructorVariants[0]->getParameters()), + false, + new VoidType(), + TemplateTypeVarianceMap::createEmpty(), + ), ]; 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, false, - $datePeriodConstructorVariants[1], + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + array_map(static fn ($parameter) => new ExtendedDummyParameter( + $parameter->getName(), + TemplateTypeHelper::resolveToBounds($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $parameter->getPhpDocType(), + $parameter->getOutType(), + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType(), + $parameter->getAttributes(), + ), $datePeriodConstructorVariants[1]->getParameters()), + false, + new VoidType(), + TemplateTypeVarianceMap::createEmpty(), + ), ]; yield [ [ @@ -120,7 +176,7 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'event|args', @@ -128,41 +184,12 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), true, - null + new NullType(), ), ], true, - new StringType() - ), - ]; - - $absVariants = $reflectionProvider->getFunction(new Name('abs'), null)->getVariants(); - yield [ - [ - new FloatType(), - new FloatType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new FloatType(), - new IntegerType(), new StringType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new StringType(), - ], - $absVariants, - false, - $absVariants[2], + ), ]; $strtokVariants = $reflectionProvider->getFunction(new Name('strtok'), null)->getVariants(); @@ -180,7 +207,7 @@ public function dataSelectFromTypes(): \Generator new StringType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'token', @@ -188,11 +215,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 +242,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'intVariadic', @@ -223,11 +250,11 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -239,7 +266,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'floatVariadic', @@ -247,11 +274,11 @@ public function dataSelectFromTypes(): \Generator new FloatType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), ]; @@ -284,11 +311,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 +327,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(2) + new ConstantIntegerType(2), ), ], false, - new NullType() + new NullType(), ), ]; @@ -327,11 +354,11 @@ public function dataSelectFromTypes(): \Generator new UnionType([ new ConstantIntegerType(1), new ConstantIntegerType(2), - ]) + ]), ), ], false, - new NullType() + new NullType(), ), ]; @@ -346,11 +373,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 +389,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -386,11 +413,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -405,16 +432,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 +461,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 */ + #[DataProvider('dataSelectFromTypes')] public function testSelectFromTypes( array $types, array $variants, bool $unpack, - ParametersAcceptor $expected + ParametersAcceptor $expected, ): void { $selectedAcceptor = ParametersAcceptorSelector::selectFromTypes($types, $variants, $unpack); @@ -463,36 +488,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 448619dee9..9c2858aaea 100644 --- a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php @@ -2,42 +2,74 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; +use stdClass; -class UniversalObjectCratesClassReflectionExtensionTest extends \PHPStan\Testing\PHPStanTestCase +class UniversalObjectCratesClassReflectionExtensionTest extends PHPStanTestCase { public function testNonexistentClass(): void { - $reflectionProvider = $this->createReflectionProvider(); - $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ - 'NonexistentClass', - 'stdClass', - ]); - $this->assertTrue($extension->hasProperty($reflectionProvider->getClass(\stdClass::class), 'foo')); + $reflectionProvider = self::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'; - $reflectionProvider = $this->createReflectionProvider(); - $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ - 'UniversalObjectCreates\DifferentGetSetTypes', - ]); + $reflectionProvider = self::createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + ['UniversalObjectCreates\DifferentGetSetTypes'], + new AnnotationsPropertiesClassReflectionExtension(), + ); $this->assertEquals( new ObjectType('UniversalObjectCreates\DifferentGetSetTypesValue'), $extension ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getReadableType() + ->getReadableType(), ); $this->assertEquals( new StringType(), $extension ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getWritableType() + ->getWritableType(), + ); + } + + public function testAnnotationOverrides(): void + { + require_once __DIR__ . '/data/universal-object-crates-annotations.php'; + $className = 'UniversalObjectCratesAnnotations\Model'; + + $reflectionProvider = self::createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + [$className], + new AnnotationsPropertiesClassReflectionExtension(), + ); + + $this->assertEquals( + new StringType(), + $extension + ->getProperty($reflectionProvider->getClass($className), 'foo') + ->getReadableType(), + ); + $this->assertEquals( + new StringType(), + $extension + ->getProperty($reflectionProvider->getClass($className), 'foo') + ->getWritableType(), ); } diff --git a/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php b/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php new file mode 100644 index 0000000000..09cf01b20b --- /dev/null +++ b/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php @@ -0,0 +1,12 @@ +> */ + public static function data(): iterable + { + $inputFile = self::getTestInputFile(); + $contents = file_get_contents($inputFile); + + if ($contents === false) { + self::fail('Input file \'' . $inputFile . '\' is missing.'); + } + + $parts = explode('-----', $contents); + + for ($i = 1; $i + 1 < count($parts); $i += 2) { + $input = trim($parts[$i]); + $output = trim($parts[$i + 1]); + + yield $input => [ + $input, + $output, + ]; + } + } + + #[DataProvider('data')] + public function test(string $input, string $expectedOutput): void + { + $output = self::generateSymbolDescription($input); + $output = trim($output); + $this->assertSame($expectedOutput, $output); + } + + private static function generateSymbolDescription(string $symbol): string + { + [$type, $name] = explode(' ', $symbol); + + if ($name === '') { + throw new ShouldNotHappenException(); + } + + try { + switch ($type) { + case 'FUNCTION': + return self::generateFunctionDescription($name); + case 'CLASS': + return self::generateClassDescription($name); + case 'METHOD': + return self::generateClassMethodDescription($name); + case 'PROPERTY': + return self::generateClassPropertyDescription($name); + default: + self::fail('Unknown symbol type ' . $type); + } + } catch (Throwable $e) { + // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + if ($e instanceof \PHPUnit\Exception) { + throw $e; + } + + // Skip stack trace - it's not fully consistent between dump and test. + return "Generating symbol description failed:\n" + . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . "\n"; + } + } + + public static function dumpOutput(): void + { + $symbolsTxt = file_get_contents(self::getPhpSymbolsFile()); + + if ($symbolsTxt === false) { + throw new ShouldNotHappenException('Cannot read phpSymbols.txt'); + } + + $symbols = explode("\n", $symbolsTxt); + $separator = '-----'; + $contents = ''; + + foreach ($symbols as $line) { + $contents .= $separator . "\n"; + $contents .= $line . "\n"; + $contents .= $separator . "\n"; + $contents .= self::generateSymbolDescription($line); + } + + $result = file_put_contents(self::getTestInputFile(), $contents); + + if ($result !== false) { + return; + } + + throw new ShouldNotHappenException('Failed write dump for reflection golden test.'); + } + + private static function getTestInputFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_TEST_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + $first = (int) floor(PHP_VERSION_ID / 10000); + $second = (int) (floor(PHP_VERSION_ID % 10000) / 100); + $currentVersion = $first . '.' . $second; + + return __DIR__ . '/data/golden/reflection-' . $currentVersion . '.test'; + } + + private static function getPhpSymbolsFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_SYMBOLS_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + return __DIR__ . '/data/golden/phpSymbols.txt'; + } + + /** + * @param non-empty-string $functionName + */ + private static function generateFunctionDescription(string $functionName): string + { + $nameNode = new Name($functionName); + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasFunction($nameNode, null)) { + return "MISSING\n"; + } + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $result = self::generateFunctionMethodBaseDescription($functionReflection); + + if (! $functionReflection->isBuiltin()) { + $result .= "NOT BUILTIN\n"; + } + + $result .= self::generateVariantsDescription($functionReflection->getName(), $functionReflection->getVariants(), false); + $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($functionReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + private static function generateClassDescription(string $className): string + { + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $result = ''; + $classReflection = $reflectionProvider->getClass($className); + + if ($classReflection->isDeprecated()) { + $result .= "Deprecated\n"; + } + + if (! $classReflection->isBuiltin()) { + $result .= "Not builtin\n"; + } + + if ($classReflection->isInternal()) { + $result .= "Internal\n"; + } + + if ($classReflection->isImmutable()) { + $result .= "Immutable\n"; + } + + if ($classReflection->hasConsistentConstructor()) { + $result .= "Consistent constructor\n"; + } + + $parentReflection = $classReflection->getParentClass(); + $extends = ''; + + if ($parentReflection !== null) { + $extends = ' extends ' . $parentReflection->getName(); + } + + $attributes = []; + + if ($classReflection->allowsDynamicProperties()) { + $attributes[] = "#[AllowDynamicProperties]\n"; + } + + $attributesTxt = implode('', $attributes); + $abstractTxt = $classReflection->isAbstract() + ? 'abstract ' + : ''; + + switch (true) { + case $classReflection->isEnum(): + $keyword = 'enum'; + break; + case $classReflection->isInterface(): + $keyword = 'interface'; + break; + case $classReflection->isTrait(): + $keyword = 'trait'; + break; + case $classReflection->isClass(): + $keyword = 'class'; + break; + default: + $keyword = self::fail(); + } + + $verbosityLevel = VerbosityLevel::precise(); + $backedEnumType = $classReflection->getBackedEnumType(); + $backedEnumTypeTxt = $backedEnumType !== null + ? ': ' . $backedEnumType->describe($verbosityLevel) + : ''; + $readonlyTxt = $classReflection->isReadOnly() + ? 'readonly ' + : ''; + $interfaceNames = array_keys($classReflection->getImmediateInterfaces()); + $implementsTxt = $interfaceNames !== [] + ? ($classReflection->isInterface() ? ' extends ' : ' implements ') . implode(', ', $interfaceNames) + : ''; + $finalTxt = $classReflection->isFinal() + ? 'final ' + : ''; + $result .= $attributesTxt . $finalTxt . $readonlyTxt . $abstractTxt . $keyword . ' ' + . $classReflection->getName() . $extends . $implementsTxt . $backedEnumTypeTxt . "\n"; + $result .= "{\n"; + $ident = ' '; + + foreach (array_keys($classReflection->getTraits()) as $trait) { + $result .= $ident . 'use ' . $trait . ";\n"; + } + + $result .= "}\n"; + + return $result; + } + + private static function generateClassMethodDescription(string $classMethodName): string + { + [$className, $methodName] = explode('::', $classMethodName); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeMethod($methodName)) { + return "MISSING\n"; + } + + $methodReflection = $classReflection->getNativeMethod($methodName); + $result = self::generateFunctionMethodBaseDescription($methodReflection); + $verbosityLevel = VerbosityLevel::precise(); + + if ($methodReflection->getSelfOutType() !== null) { + $result .= 'Self out type: ' . $methodReflection->getSelfOutType()->describe($verbosityLevel) . "\n"; + } + + if ($methodReflection->isStatic()) { + $result .= "Static\n"; + } + + switch (true) { + case $methodReflection->isPublic(): + $visibility = 'public'; + break; + case $methodReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $result .= self::generateVariantsDescription($methodReflection->getName(), $methodReflection->getVariants(), false); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($methodReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + /** @param FunctionReflection|ExtendedMethodReflection $reflection */ + private static function generateFunctionMethodBaseDescription($reflection): string + { + $result = ''; + + if (! $reflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $reflection->isDeprecated()->describe() . "\n"; + } + + if ($reflection instanceof MethodReflection && ! $reflection->isFinal()->no()) { + $result .= 'Is final: ' . $reflection->isFinal()->describe() . "\n"; + } + + if (! $reflection->isInternal()->no()) { + $result .= 'Is internal: ' . $reflection->isInternal()->describe() . "\n"; + } + + if (is_bool($reflection->isBuiltin()) && $reflection->isBuiltin()) { + $result .= 'Is built-in' . "\n"; + } + + if (!is_bool($reflection->isBuiltin()) && !$reflection->isBuiltin()->no()) { + $result .= 'Is built-in: ' . $reflection->isBuiltin()->describe() . "\n"; + } + + if (! $reflection->returnsByReference()->no()) { + $result .= 'Returns by reference: ' . $reflection->returnsByReference()->describe() . "\n"; + } + + if (! $reflection->hasSideEffects()->no()) { + $result .= 'Has side-effects: ' . $reflection->hasSideEffects()->describe() . "\n"; + } + + if ($reflection->getThrowType() !== null) { + $result .= 'Throw type: ' . $reflection->getThrowType()->describe(VerbosityLevel::precise()) . "\n"; + } + + return $result; + } + + /** @param ExtendedParametersAcceptor[] $variants */ + private static function generateVariantsDescription(string $name, array $variants, bool $isNamedArguments): string + { + $variantCount = count($variants); + $result = $isNamedArguments + ? 'Named arguments variants: ' + : 'Variants: '; + $result .= $variantCount . "\n"; + $variantIdent = ' '; + $verbosityLevel = VerbosityLevel::precise(); + + foreach ($variants as $variant) { + $paramsNative = []; + $paramsPhpDoc = []; + + foreach ($variant->getParameters() as $param) { + $paramsPhpDoc[] = $variantIdent . ' * @param ' . $param->getType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + + if ($param->getOutType() !== null) { + $paramsPhpDoc[] = $variantIdent . ' * @param-out ' . $param->getOutType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + } + + $passedByRef = $param->passedByReference(); + + if ($passedByRef->no()) { + $refDes = ''; + } elseif ($passedByRef->createsNewVariable()) { + $refDes = '&rw'; + } else { + $refDes = '&r'; + } + + $variadicDesc = $param->isVariadic() ? '...' : ''; + $defValueDesc = $param->getDefaultValue() !== null + ? ' = ' . $param->getDefaultValue()->describe($verbosityLevel) + : ''; + + $paramsNative[] = $param->getNativeType()->describe($verbosityLevel) . ' ' . $variadicDesc . $refDes . '$' . $param->getName() . $defValueDesc; + } + + $result .= $variantIdent . "/**\n"; + $result .= implode('', $paramsPhpDoc); + $result .= $variantIdent . ' * @return ' . $variant->getReturnType()->describe($verbosityLevel) . "\n"; + $result .= $variantIdent . " */\n"; + $paramsTxt = implode(', ', $paramsNative); + $result .= $variantIdent . 'function ' . $name . '(' . $paramsTxt . '): ' . $variant->getNativeReturnType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + private static function generateClassPropertyDescription(string $propertyName): string + { + [$className, $propertyName] = explode('::', $propertyName); + // remove $ + $propertyName = substr($propertyName, 1); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeProperty($propertyName)) { + return "MISSING\n"; + } + + $result = ''; + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + if (! $propertyReflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if (! $propertyReflection->isInternal()->no()) { + $result .= 'Is internal: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if ($propertyReflection->isStatic()) { + $result .= "Static\n"; + } + + if ($propertyReflection->isReadOnly()) { + $result .= "Readonly\n"; + } + + switch (true) { + case $propertyReflection->isPublic(): + $visibility = 'public'; + break; + case $propertyReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $verbosityLevel = VerbosityLevel::precise(); + + if ($propertyReflection->isReadable()) { + $result .= 'Read type: ' . $propertyReflection->getReadableType()->describe($verbosityLevel) . "\n"; + } + + if ($propertyReflection->isWritable()) { + $result .= 'Write type: ' . $propertyReflection->getWritableType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + public static function dumpInputSymbols(): void + { + $symbols = self::scrapeInputSymbols(); + $symbolsFile = self::getPhpSymbolsFile(); + @mkdir(dirname($symbolsFile), recursive: 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; + + #[Override] + 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; + } + + #[Override] + public function leaveNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassLike) { + unset($this->classLike); + } + + return null; + } + + }; + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + + foreach ($stubFiles as $file) { + $ast = $parser->parseFile($file); + $traverser->traverse($ast); + } + + return $visitor->symbols; + } + +} diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index 7af3d45fc7..2e538e5f32 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -2,34 +2,44 @@ namespace PHPStan\Reflection; +use DateTime; use PhpParser\Node\Name; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; class ReflectionProviderTest extends PHPStanTestCase { - public function dataFunctionThrowType(): iterable + public static function dataFunctionThrowType(): iterable { yield [ 'rand', null, ]; - if (PHP_VERSION_ID >= 70200) { + yield [ + 'sodium_crypto_kx_keypair', + new ObjectType('SodiumException'), + ]; + + if (PHP_VERSION_ID >= 80000) { yield [ - 'sodium_crypto_kx_keypair', - new ObjectType('SodiumException'), + 'bcdiv', + new ObjectType('DivisionByZeroError'), + ]; + } else { + yield [ + 'bcdiv', + null, ]; } - yield [ - 'bcdiv', - new ObjectType('DivisionByZeroError'), - ]; - yield [ 'GEOSRelateMatch', new ObjectType('Exception'), @@ -37,18 +47,17 @@ public function dataFunctionThrowType(): iterable yield [ 'random_int', - new ObjectType('Exception'), + new ObjectType('Random\RandomException'), ]; } /** - * @dataProvider dataFunctionThrowType - * @param string $functionName - * @param ?Type $expectedThrowType + * @param non-empty-string $functionName */ + #[DataProvider('dataFunctionThrowType')] public function testFunctionThrowType(string $functionName, ?Type $expectedThrowType): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $function = $reflectionProvider->getFunction(new Name($functionName), null); $throwType = $function->getThrowType(); if ($expectedThrowType === null) { @@ -58,35 +67,67 @@ public function testFunctionThrowType(string $functionName, ?Type $expectedThrow $this->assertNotNull($throwType); $this->assertSame( $expectedThrowType->describe(VerbosityLevel::precise()), - $throwType->describe(VerbosityLevel::precise()) + $throwType->describe(VerbosityLevel::precise()), ); } - public function dataMethodThrowType(): array + public static 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, + ]; + } + + /** + * @param non-empty-string $functionName + */ + #[DataProvider('dataFunctionDeprecated')] + public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $this->assertEquals(TrinaryLogic::createFromBoolean($isDeprecated), $function->isDeprecated()); + } + + public static function dataMethodThrowType(): array { return [ [ - \DateTime::class, + DateTime::class, '__construct', - new ObjectType('Exception'), + PHP_VERSION_ID >= 80300 ? new ObjectType('DateMalformedStringException') : new ObjectType('Exception'), ], [ - \DateTime::class, + DateTime::class, 'format', null, ], ]; } - /** - * @dataProvider dataMethodThrowType - * @param string $className - * @param string $methodName - * @param ?Type $expectedThrowType - */ + #[DataProvider('dataMethodThrowType')] public function testMethodThrowType(string $className, string $methodName, ?Type $expectedThrowType): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass($className); $method = $class->getNativeMethod($methodName); $throwType = $method->getThrowType(); @@ -97,8 +138,19 @@ public function testMethodThrowType(string $className, string $methodName, ?Type $this->assertNotNull($throwType); $this->assertSame( $expectedThrowType->describe(VerbosityLevel::precise()), - $throwType->describe(VerbosityLevel::precise()) + $throwType->describe(VerbosityLevel::precise()), ); } + #[RequiresPhp('>= 8.3')] + public function testNativeClassConstantTypeInEvaledClass(): void + { + eval('namespace NativeClassConstantInEvaledClass; class Foo { public const int FOO = 1; }'); + + $reflectionProvider = self::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 891505b8db..24ef8431ee 100644 --- a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php @@ -18,7 +18,7 @@ public function testSchema(): void $processor->process(Expect::arrayOf( Expect::structure([ 'hasSideEffects' => Expect::bool()->required(), - ])->required() + ])->required(), )->required(), $data); } diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index eab83934f2..8c422e1500 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -2,20 +2,29 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\Php\PhpVersion; use PHPStan\Php8StubsMap; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -25,11 +34,16 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function array_merge; +use function count; +use const PHP_VERSION_ID; class Php8SignatureMapProviderTest extends PHPStanTestCase { - public function dataFunctions(): array + public static function dataFunctions(): array { return [ [ @@ -50,7 +64,7 @@ public function dataFunctions(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('CurlHandle'), new ConstantBooleanType(false), ]), @@ -87,15 +101,15 @@ public function dataFunctions(): array new ConstantStringType('error_count'), new ConstantStringType('errors'), ], [ - new IntegerType(), - new ArrayType(new IntegerType(), new StringType()), - new IntegerType(), - new ArrayType(new IntegerType(), new StringType()), + IntegerRangeType::fromInterval(0, null), + new IntersectionType([new ArrayType(IntegerRangeType::fromInterval(0, null), new StringType()), new AccessoryArrayListType()]), + IntegerRangeType::fromInterval(0, null), + new IntersectionType([new ArrayType(IntegerRangeType::fromInterval(0, null), new StringType()), new AccessoryArrayListType()]), ]), ]), new UnionType([ new ConstantBooleanType(false), - new ArrayType(new MixedType(true), new MixedType(true)), + new ArrayType(new MixedType(), new MixedType()), ]), false, ], @@ -119,35 +133,44 @@ public function dataFunctions(): array } /** - * @dataProvider dataFunctions * @param mixed[] $parameters */ + #[DataProvider('dataFunctions')] public function testFunctions( string $functionName, array $parameters, Type $returnType, Type $nativeReturnType, - bool $variadic + bool $variadic, ): void { $provider = $this->createProvider(); - $signature = $provider->getFunctionSignature($functionName, null); - $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signature); + $reflector = self::getContainer()->getByType(Reflector::class); + $signatures = $provider->getFunctionSignatures($functionName, null, new ReflectionFunction($reflector->reflectFunction($functionName)))['positional']; + $this->assertCount(1, $signatures); + $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } private function createProvider(): Php8SignatureMapProvider { + $phpVersion = new PhpVersion(80000); + return new Php8SignatureMapProvider( new FunctionSignatureMapProvider( self::getContainer()->getByType(SignatureMapParser::class), - new PhpVersion(80000) + self::getContainer()->getByType(InitializerExprTypeResolver::class), + $phpVersion, + true, ), self::getContainer()->getByType(FileNodesFetcher::class), - self::getContainer()->getByType(FileTypeMapper::class) + self::getContainer()->getByType(FileTypeMapper::class), + $phpVersion, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getContainer()->getByType(ReflectionProviderProvider::class), ); } - public function dataMethods(): array + public static function dataMethods(): array { return [ [ @@ -173,7 +196,8 @@ public function dataMethods(): array 'optional' => true, 'type' => new UnionType([ new ObjectWithoutClassType(), - new StringType(), + new ClassStringType(), + new ConstantStringType('static'), new NullType(), ]), 'nativeType' => new UnionType([ @@ -185,7 +209,7 @@ public function dataMethods(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('Closure'), new NullType(), ]), @@ -239,36 +263,33 @@ public function dataMethods(): array } /** - * @dataProvider dataMethods * @param mixed[] $parameters */ + #[DataProvider('dataMethods')] public function testMethods( string $className, string $methodName, array $parameters, Type $returnType, Type $nativeReturnType, - bool $variadic + bool $variadic, ): void { $provider = $this->createProvider(); - $signature = $provider->getMethodSignature($className, $methodName, null); - $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signature); + $signatures = $provider->getMethodSignatures($className, $methodName, null)['positional']; + $this->assertCount(1, $signatures); + $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } /** * @param mixed[] $expectedParameters - * @param Type $expectedReturnType - * @param Type $expectedNativeReturnType - * @param bool $expectedVariadic - * @param FunctionSignature $actualSignature */ private function assertSignature( array $expectedParameters, Type $expectedReturnType, Type $expectedNativeReturnType, bool $expectedVariadic, - FunctionSignature $actualSignature + FunctionSignature $actualSignature, ): void { $this->assertCount(count($expectedParameters), $actualSignature->getParameters()); @@ -287,17 +308,13 @@ private function assertSignature( $this->assertSame($expectedVariadic, $actualSignature->isVariadic()); } - public function dataParseAll(): array + public static function dataParseAll(): array { - return array_map(static function (string $file): array { - return [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file]; - }, array_merge(Php8StubsMap::CLASSES, Php8StubsMap::FUNCTIONS)); + $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 - * @param string $stubFile - */ + #[DataProvider('dataParseAll')] public function testParseAll(string $stubFile): void { $parser = $this->getParser(); diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index 78d73a9f57..98b47e6cb0 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -2,8 +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; @@ -17,13 +28,21 @@ use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +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\PHPStanTestCase +class SignatureMapParserTest extends PHPStanTestCase { - public function dataGetFunctions(): array + public static function dataGetFunctions(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [ ['int', 'fp' => 'resource', 'fields' => 'array', 'delimiter=' => 'string', 'enclosure=' => 'string', 'escape_char=' => 'string'], @@ -36,7 +55,9 @@ public function dataGetFunctions(): array new ResourceType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'fields', @@ -44,7 +65,9 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'delimiter', @@ -52,7 +75,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'enclosure', @@ -60,7 +85,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'escape_char', @@ -68,12 +95,14 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new IntegerType(), new MixedType(), - false + false, ), ], [ @@ -87,12 +116,14 @@ public function dataGetFunctions(): array new ResourceType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -106,12 +137,14 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createReadsArgument(), - false + false, + null, + null, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -128,7 +161,9 @@ public function dataGetFunctions(): array ]), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'out', @@ -136,7 +171,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createCreatesNewVariable(), - false + false, + null, + null, ), new ParameterSignature( 'notext', @@ -144,12 +181,14 @@ public function dataGetFunctions(): array new BooleanType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -158,12 +197,12 @@ public function dataGetFunctions(): array new FunctionSignature( [], new UnionType([ - new ObjectType(\Throwable::class), + new ObjectType(Throwable::class), new ObjectType('Foo'), new NullType(), ]), new MixedType(), - false + false, ), ], [ @@ -173,7 +212,7 @@ public function dataGetFunctions(): array [], new MixedType(), new MixedType(), - false + false, ), ], [ @@ -187,7 +226,9 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'arr2', @@ -195,7 +236,9 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( '...', @@ -203,12 +246,14 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new ArrayType(new MixedType(), new MixedType()), new MixedType(), - true + true, ), ], [ @@ -222,7 +267,9 @@ public function dataGetFunctions(): array new CallableType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'event', @@ -230,7 +277,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( '...', @@ -238,12 +287,14 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new ResourceType(), new MixedType(), - true + true, ), ], [ @@ -257,7 +308,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'args', @@ -265,12 +318,14 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new StringType(), new MixedType(), - true + true, ), ], [ @@ -284,7 +339,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'args', @@ -292,12 +349,14 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new StringType(), new MixedType(), - true + true, ), ], [ @@ -305,28 +364,30 @@ public function dataGetFunctions(): array null, new FunctionSignature( [], - new ArrayType(new IntegerType(), new ObjectType(\ReflectionParameter::class)), + new ArrayType(new IntegerType(), new ObjectType(ReflectionParameter::class)), new MixedType(), - false + false, ), ], [ ['static', 'interval' => 'DateInterval'], - \DateTime::class, + DateTime::class, new FunctionSignature( [ new ParameterSignature( 'interval', false, - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], - new StaticType($reflectionProvider->getClass(\DateTime::class)), + new StaticType($reflectionProvider->getClass(DateTime::class)), new MixedType(), - false + false, ), ], [ @@ -340,7 +401,9 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createReadsArgument(), - false + false, + null, + null, ), new ParameterSignature( 'strings', @@ -348,27 +411,27 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createReadsArgument(), - true + true, + null, + null, ), ], new BooleanType(), new MixedType(), - true + true, ), ], ]; } /** - * @dataProvider dataGetFunctions * @param mixed[] $map - * @param string|null $className - * @param \PHPStan\Reflection\SignatureMap\FunctionSignature $expectedSignature */ + #[DataProvider('dataGetFunctions')] public function testGetFunctions( array $map, ?string $className, - FunctionSignature $expectedSignature + FunctionSignature $expectedSignature, ): void { /** @var SignatureMapParser $parser */ @@ -377,7 +440,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) { @@ -385,42 +448,42 @@ 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 dataParseAll(): array + public static function dataParseAll(): array { return [ [70400], @@ -428,15 +491,13 @@ public function dataParseAll(): array ]; } - /** - * @dataProvider dataParseAll - * @param int $phpVersionId - */ + #[DataProvider('dataParseAll')] public function testParseAll(int $phpVersionId): void { $parser = self::getContainer()->getByType(SignatureMapParser::class); - $provider = new FunctionSignatureMapProvider($parser, new PhpVersion($phpVersionId)); + $provider = new FunctionSignatureMapProvider($parser, self::getContainer()->getByType(InitializerExprTypeResolver::class), new PhpVersion($phpVersionId), true); $signatureMap = $provider->getSignatureMap(); + $reflector = self::getContainer()->getByType(Reflector::class); $count = 0; foreach (array_keys($signatureMap) as $functionName) { @@ -444,24 +505,52 @@ public function testParseAll(int $phpVersionId): void if (strpos($functionName, '::') !== false) { $parts = explode('::', $functionName); $className = $parts[0]; + $realFunctionName = $parts[1]; + } else { + $realFunctionName = $functionName; + } + + if (strpos($realFunctionName, "'") !== false) { + continue; + } + + if ($realFunctionName === '') { + throw new ShouldNotHappenException(); + } + + $reflectionFunction = null; + + try { + if ($className !== null) { + $method = $reflector->reflectClass($className)->getMethod($realFunctionName); + if ($method !== null) { + $reflectionFunction = new ReflectionMethod($method); + } + } else { + $reflectionFunction = new ReflectionFunction($reflector->reflectFunction($realFunctionName)); + } + } catch (IdentifierNotFound | OutOfBoundsException $e) { + // pass } try { - $signature = $provider->getFunctionSignature($functionName, $className); - $count++; - } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { + $signatures = $provider->getFunctionSignatures($functionName, $className, $reflectionFunction)['positional']; + $count += count($signatures); + } catch (ParserException $e) { $this->fail(sprintf('Could not parse %s: %s.', $functionName, $e->getMessage())); } - self::assertNotInstanceOf(ErrorType::class, $signature->getReturnType(), $functionName); - $optionalOcurred = false; - foreach ($signature->getParameters() as $parameter) { - if ($parameter->isOptional()) { - $optionalOcurred = true; - } elseif ($optionalOcurred) { - $this->fail(sprintf('%s contains required parameter after optional.', $functionName)); + foreach ($signatures as $signature) { + self::assertNotInstanceOf(ErrorType::class, $signature->getReturnType(), $functionName); + $optionalOcurred = false; + foreach ($signature->getParameters() as $parameter) { + if ($parameter->isOptional()) { + $optionalOcurred = true; + } elseif ($optionalOcurred) { + $this->fail(sprintf('%s contains required parameter after optional.', $functionName)); + } + self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName())); } - self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName())); } } diff --git a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php index 191aa71e2d..2d7d3ca357 100644 --- a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php @@ -2,10 +2,11 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; -class IntersectionTypeMethodReflectionTest extends \PHPStan\Testing\PHPStanTestCase +class IntersectionTypeMethodReflectionTest extends PHPStanTestCase { public function testCollectsDeprecatedMessages(): void @@ -16,7 +17,7 @@ public function testCollectsDeprecatedMessages(): void $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), - ] + ], ); $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); @@ -29,15 +30,15 @@ public function testMultipleDeprecationsAreJoined(): void [ $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): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php index b17b962fb0..b41d8d9636 100644 --- a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php @@ -2,10 +2,11 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; -class UnionTypeMethodReflectionTest extends \PHPStan\Testing\PHPStanTestCase +class UnionTypeMethodReflectionTest extends PHPStanTestCase { public function testCollectsDeprecatedMessages(): void @@ -16,7 +17,7 @@ public function testCollectsDeprecatedMessages(): void $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), - ] + ], ); $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); @@ -29,15 +30,15 @@ public function testMultipleDeprecationsAreJoined(): void [ $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): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/UnionTypesTest.php b/tests/PHPStan/Reflection/UnionTypesTest.php index 5bab2ee37d..8b72103e33 100644 --- a/tests/PHPStan/Reflection/UnionTypesTest.php +++ b/tests/PHPStan/Reflection/UnionTypesTest.php @@ -13,20 +13,16 @@ class UnionTypesTest extends PHPStanTestCase public function testUnionTypes(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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 = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $methodVariant = $method->getOnlyVariant(); $methodReturnType = $methodVariant->getReturnType(); $this->assertInstanceOf(UnionType::class, $methodReturnType); $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $methodReturnType->describe(VerbosityLevel::precise())); @@ -36,7 +32,7 @@ public function testUnionTypes(): void $this->assertSame('bool|int', $methodParameterType->describe(VerbosityLevel::precise())); $function = $reflectionProvider->getFunction(new Name('NativeUnionTypes\doFoo'), null); - $functionVariant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + $functionVariant = $function->getOnlyVariant(); $functionReturnType = $functionVariant->getReturnType(); $this->assertInstanceOf(UnionType::class, $functionReturnType); $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $functionReturnType->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php new file mode 100644 index 0000000000..e4a0c5e696 --- /dev/null +++ b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php @@ -0,0 +1,10 @@ +getName() === 'AllowedSubTypesClassReflectionExtensionTest\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Bar'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Baz'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Qux'), + ]; + } +} + +function acceptsFoo(Foo $foo): void { + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo', $foo); + + if ($foo instanceof Bar) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo~AllowedSubTypesClassReflectionExtensionTest\\Bar', $foo); + + if ($foo instanceof Qux) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Baz', $foo); +} diff --git a/tests/PHPStan/Reflection/data/anonymous-classes.php b/tests/PHPStan/Reflection/data/anonymous-classes.php new file mode 100644 index 0000000000..4024445aec --- /dev/null +++ b/tests/PHPStan/Reflection/data/anonymous-classes.php @@ -0,0 +1,33 @@ += 8.1 + +namespace AttributeReflectionTest; + +enum FooEnum +{ + + #[MyAttr(one: 15, two: 16)] + case TEST; + +} diff --git a/tests/PHPStan/Reflection/data/attribute-reflection.php b/tests/PHPStan/Reflection/data/attribute-reflection.php new file mode 100644 index 0000000000..34ec36599f --- /dev/null +++ b/tests/PHPStan/Reflection/data/attribute-reflection.php @@ -0,0 +1,62 @@ += 8.0 + +namespace AttributeReflectionTest; + +use Attribute; + +#[Attribute] +class MyAttr +{ + + public function __construct($one, $two) + { + + } + +} + +#[MyAttr(1, 2)] +class Foo +{ + + #[MyAttr(one: 3, two: 4)] + public const MY_CONST = 1; + + #[MyAttr(two: 6, one: 5)] + private $prop; + + #[MyAttr(7, 8)] + public function __construct( + #[MyAttr(9, 10)] + int $test + ) + { + + } + +} + +#[MyAttr()] +function myFunction() { + +} + +#[Nonexistent()] +function myFunction2() { + +} + +#[Nonexistent(1, 2)] +function myFunction3() { + +} + +#[MyAttr(11, 12)] +function myFunction4() { + +} + +#[MyAttr(28, two: 29)] +function myFunction5() { + +} diff --git a/tests/PHPStan/Reflection/data/function-definitions.php b/tests/PHPStan/Reflection/data/function-definitions.php deleted file mode 100644 index 497c3dbe71..0000000000 --- a/tests/PHPStan/Reflection/data/function-definitions.php +++ /dev/null @@ -1,8 +0,0 @@ -= 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 @@ + + * @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..b139a1ef2a --- /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(), self::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 index f85489374d..f62d2c2225 100644 --- a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,9 +12,9 @@ class ApiClassExtendsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ApiClassExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiClassExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/class-extends-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index abba2f945c..7d3246d014 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,9 +12,9 @@ class ApiClassImplementsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ApiClassImplementsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiClassImplementsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -24,18 +26,38 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/class-implements-out-of-phpstan.php'], [ [ 'Implementing PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 16, + 20, $tip, ], [ 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 50, + 54, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 363, + $tip, + ], + [ + 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 368, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 373, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 377, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php new file mode 100644 index 0000000000..edba6b82e1 --- /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(), self::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..08ceeff3fd --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiInstanceofTypeRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ApiInstanceofTypeRule(self::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 index 27311eafd9..e2e968aadd 100644 --- a/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,11 +12,11 @@ class ApiInstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiInstantiationRule( new ApiRuleHelper(), - $this->createReflectionProvider() + self::createReflectionProvider(), ); } @@ -27,7 +29,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/new-out-of-phpstan.php'], [ [ diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php index b15ce60be8..ee499665b8 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,9 +12,9 @@ class ApiInterfaceExtendsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ApiInterfaceExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiInterfaceExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -24,13 +26,28 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/interface-extends-out-of-phpstan.php'], [ [ 'Extending PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 8, + 10, + $tip, + ], + [ + 'Extending PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 20, + $tip, + ], + [ + 'Extending PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 25, + $tip, + ], + [ + 'Extending PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 30, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php index 1707090bca..d91f116610 100644 --- a/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiMethodCallRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiMethodCallRule(new ApiRuleHelper()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/method-call-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php index 4ff44e26b5..81945b88e6 100644 --- a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php +++ b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Api; use PHPStan\Analyser\Scope; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ApiRuleHelperTest extends TestCase { - public function dataIsPhpStanCode(): array + public static function dataIsPhpStanCode(): array { return [ [ @@ -133,15 +134,13 @@ public function dataIsPhpStanCode(): array ]; } - /** - * @dataProvider dataIsPhpStanCode - */ + #[DataProvider('dataIsPhpStanCode')] public function testIsPhpStanCode( ?string $scopeNamespace, string $scopeFile, string $nameToCheck, ?string $declaringFileNameToCheck, - bool $expected + bool $expected, ): void { $rule = new ApiRuleHelper(); diff --git a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php index 442eb0fb11..26da521670 100644 --- a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,9 +12,9 @@ class ApiStaticCallRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ApiStaticCallRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiStaticCallRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/static-call-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php index 96a2060067..d15bbf5aa1 100644 --- a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,9 +12,9 @@ class ApiTraitUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ApiTraitUseRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiTraitUseRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/trait-use-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php new file mode 100644 index 0000000000..fab3cc8528 --- /dev/null +++ b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class GetTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new GetTemplateTypeRule(self::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 index 38db282fdc..aaa875710d 100644 --- a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php +++ b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php @@ -3,8 +3,11 @@ namespace PHPStan\Rules\Api; use Nette\Utils\Json; +use Override; use PHPStan\File\FileWriter; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function unlink; /** * @extends RuleTestCase @@ -12,11 +15,12 @@ class PhpStanNamespaceIn3rdPartyPackageRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new PhpStanNamespaceIn3rdPartyPackageRule(new ApiRuleHelper()); } + #[Override] protected function tearDown(): void { @unlink(__DIR__ . '/composer.json'); diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php new file mode 100644 index 0000000000..a1b0203897 --- /dev/null +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php @@ -0,0 +1,45 @@ + + */ +class RuntimeReflectionFunctionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RuntimeReflectionFunctionRule(self::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..0ec03856c5 --- /dev/null +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php @@ -0,0 +1,83 @@ + + */ +class RuntimeReflectionInstantiationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RuntimeReflectionInstantiationRule(self::createReflectionProvider()); + } + + public function testRule(): void + { + $errors = [ + [ + 'Creating new ReflectionMethod is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 43, + ], + [ + 'Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 44, + ], + [ + 'Creating new ReflectionClassConstant is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 45, + ], + [ + 'Creating new ReflectionZendExtension is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 48, + ], + [ + 'Creating new ReflectionExtension is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 49, + ], + [ + 'Creating new ReflectionFunction is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 50, + ], + [ + 'Creating new ReflectionObject is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 51, + ], + [ + 'Creating new ReflectionParameter is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 52, + ], + [ + 'Creating new ReflectionProperty is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 53, + ], + [ + 'Creating new ReflectionGenerator is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 54, + ], + ]; + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Creating new ReflectionFiber is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 55, + ]; + } + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ + 'Creating new ReflectionEnum is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 56, + ]; + $errors[] = [ + 'Creating new ReflectionEnumBackedCase is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 57, + ]; + } + $this->analyse([__DIR__ . '/data/runtime-reflection-instantiation.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php b/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php new file mode 100644 index 0000000000..2879583dad --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php @@ -0,0 +1,17 @@ +getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + + public function doBar(ObjectType $type): void + { + $type->getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php b/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php new file mode 100644 index 0000000000..bb1ca7a370 --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php @@ -0,0 +1,22 @@ +getFunction(); + if ($function instanceof MethodReflection) { + + } + } + + public function doBaz($mixed): void + { + if ($mixed instanceof Type) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Api/data/instanceof-type.php b/tests/PHPStan/Rules/Api/data/instanceof-type.php new file mode 100644 index 0000000000..eabc5f6c5e --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/instanceof-type.php @@ -0,0 +1,45 @@ +getAttribute("parent"); + $custom = $node->getAttribute("myCustomAttribute"); + $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 + ParametersAcceptorSelector::selectFromArgs($f->getVariants()); // @api above class ScopeContext::create(__DIR__ . '/test.php'); // @api above method } diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php deleted file mode 100644 index d6c27680be..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ -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.', - 27, - ], - [ - 'Array (array) does not accept string.', - 32, - ], - [ - 'Array (array) does not accept Closure(): 1.', - 45, - ], - [ - 'Array (array) does not accept AppendedArrayItem\Baz.', - 79, - ], - ] - ); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php deleted file mode 100644 index ac402f3399..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php +++ /dev/null @@ -1,69 +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, - ], - [ - 'Array (array<1|2|3, string>) does not accept key int.', - 80, - ], - [ - 'Array (array<1|2|3, string>) does not accept key 4.', - 85, - ], - ]); - } - - public function testBug5372Two(): void - { - $this->analyse([__DIR__ . '/data/bug-5372_2.php'], []); - } - - public function testBug5447(): void - { - $this->analyse([__DIR__ . '/data/bug-5447.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index 195dbf6e94..b105abcd36 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,11 +15,11 @@ class ArrayDestructuringRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true); return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true) + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false), ); } @@ -30,7 +31,7 @@ public function testRule(): void 11, ], [ - 'Offset 0 does not exist on array().', + 'Offset 0 does not exist on array{}.', 12, ], [ @@ -38,14 +39,25 @@ public function testRule(): void 13, ], [ - 'Offset 2 does not exist on array(1, 2).', + 'Offset 2 does not exist on array{1, 2}.', 15, ], [ - 'Offset \'a\' does not exist on array(\'b\' => 1).', + 'Offset \'a\' does not exist on array{b: 1}.', 22, ], ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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..826d3e33f1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php @@ -0,0 +1,127 @@ + + */ +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(self::createReflectionProvider(), true, false, $this->checkUnions, false, false, $this->checkBenevolentUnions, true), + ); + } + + #[RequiresPhp('< 8.1')] + public function testRule(): void + { + $this->checkUnions = true; + $this->checkBenevolentUnions = true; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on an array with potential string keys: array{foo: \'bar\', 0: 1, 1: 2, 2: 3}', + 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, + ], + ]); + } + + #[RequiresPhp('< 8.1')] + public function testRuleDoNotCheckBenevolentUnion(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('< 8.1')] + public function testRuleDoNotCheckUnions(): void + { + $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 static function dataRuleOnPHP81(): array + { + return [ + [true], + [false], + ]; + } + + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataRuleOnPHP81')] + public function testRuleOnPHP81(bool $checkUnions): void + { + $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..9e272b5802 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,29 @@ 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'], []); + } + + public function testBug13248(): void + { + $this->analyse([__DIR__ . '/data/bug-13248.php'], []); + } + + public function testBug2560(): void + { + $this->analyse([__DIR__ . '/data/bug-2560.php'], []); + } + + public function testBug2457(): void + { + $this->analyse([__DIR__ . '/data/bug-2457.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 28e7898337..03a01b9250 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,7 +45,69 @@ 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, + ], + [ + "Array has 2 duplicate keys with value 'bar' (\$key, 'bar').", + 128, + ], + [ + "Array has 2 duplicate keys with value 'bar' (\$key, 'bar').", + 139, + ], + [ + "Array has 2 duplicate keys with value 'foo' ('foo', \$key).", + 151, + ], + [ + "Array has 2 duplicate keys with value 'bar' ('bar', \$key).", + 152, + ], + [ + "Array has 2 duplicate keys with value 'baz' (\$key, 'baz').", + 171, + ], + [ + "Array has 5 duplicate keys with value 1 (1, '1', true, 1.0, 1.1).", + 179, + ], ]); } + public function testBug13013(): void + { + $this->analyse([__DIR__ . '/data/bug-13013.php'], []); + } + + public function testBug13022(): void + { + $this->analyse([__DIR__ . '/data/bug-13022.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php deleted file mode 100644 index 51ba629e16..0000000000 --- a/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class EmptyArrayItemRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new EmptyArrayItemRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/empty-array-item.php'], [ - [ - 'Literal array contains empty item.', - 5, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index e1cc3685d0..22716afcd4 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 PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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(self::createReflectionProvider(), true, false, true, true, true, false, true); + return new InvalidKeyInArrayDimFetchRule($ruleLevelHelper, true); } public function testInvalidKey(): void @@ -32,6 +38,81 @@ public function testInvalidKey(): void 'Invalid array key type DateTimeImmutable.', 31, ], + [ + 'Possibly invalid array key type mixed.', + 41, + ], + [ + '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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6315(): void + { + $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, + ], + ]); + } + + public function testBug13135(): void + { + $this->analyse([__DIR__ . '/data/bug-13135.php'], [ + [ + 'Possibly invalid array key type Tk of mixed.', + 15, + ], + ]); + } + + public function testBug12273(): void + { + $this->analyse([__DIR__ . '/data/bug-12273.php'], [ + [ + 'Possibly invalid array key type mixed.', + 16, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 7bb10e9e47..ab486eeacf 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 PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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,15 @@ public function testInvalidKeyShortArray(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testInvalidKeyEnum(): void + { + $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 6025adb242..e00b70e84e 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -2,17 +2,27 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use function usort; /** - * @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(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testCheckWithMaybes(): void @@ -23,7 +33,7 @@ public function testCheckWithMaybes(): void 10, ], [ - 'Argument of an invalid type array|false supplied for foreach, only iterables are supported.', + 'Argument of an invalid type list|false supplied for foreach, only iterables are supported.', 19, ], [ @@ -34,4 +44,105 @@ public function testCheckWithMaybes(): void ]); } + public function testBug5744(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5744.php'], [ + /*[ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 15, + ],*/ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 28, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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 static 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, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/foreach-mixed.php'], $errors); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13312(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13312.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 30277d2480..e5d17aba80 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -2,22 +2,35 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +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, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), - true + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), + true, ); } @@ -25,15 +38,15 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ [ - 'Offset \'b\' does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', 17, ], [ - 'Offset 1 does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset 1 does not exist on array{a: stdClass, 0: 2}.', 18, ], [ - 'Offset \'a\' does not exist on array(\'b\' => 1).', + 'Offset \'a\' does not exist on array{b: 1}.', 55, ], [ @@ -79,40 +92,36 @@ 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(): void.', 253, ], [ - 'Cannot access offset \'a\' on array(\'a\' => 1, \'b\' => 1)|(Closure(): void).', + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', 258, ], [ - 'Offset string does not exist on array.', - 308, + 'Offset int|null might not exist on array.', + 309, ], [ 'Offset null does not exist on array.', @@ -123,7 +132,11 @@ public function testRule(): void 312, ], [ - 'Offset \'baz\' does not exist on array(\'bar\' => 1, ?\'baz\' => 2).', + 'Offset int|null might not exist on array.', + 314, + ], + [ + 'Offset \'baz\' might not exist on array{bar: 1, baz?: 2}.', 344, ], [ @@ -158,6 +171,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, + ], ]); } @@ -173,13 +194,21 @@ public function testStrings(): void 13, ], [ - 'Offset \'foo\' does not exist on array|string.', + 'Offset int|object might not exist on \'foo\'.', + 16, + ], + [ + '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, ], + [ + 'Offset int|object might not exist on array|string.', + 32, + ], ]); } @@ -187,7 +216,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, ], [ @@ -227,9 +256,6 @@ public function testAssignOp(): void public function testCoalesceAssign(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/nonexistent-offset-coalesce-assign.php'], []); } @@ -242,7 +268,7 @@ public function testBug3782(): void { $this->analyse([__DIR__ . '/data/bug-3782.php'], [ [ - 'Cannot access offset (int|string) on Bug3782\HelloWorld.', + 'Cannot access offset (int|string) on $this(Bug3782\HelloWorld)|(ArrayAccess&Bug3782\HelloWorld).', 11, ], ]); @@ -314,4 +340,695 @@ public function testBug5669(): void ]); } + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug4885(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7763(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-mixed.php'], [ + [ + 'Cannot access offset 5 on T of mixed.', + 11, + ], + [ + 'Cannot access offset 5 on mixed.', + 16, + ], + [ + 'Cannot access offset 5 on mixed.', + 21, + ], + ]); + } + + public function 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 static 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, + ], + ]]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataReportPossiblyNonexistentArrayOffset')] + 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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testInternalClassesWithOverloadedOffsetAccess84(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void + { + $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 testBug4809(): void + { + $this->analyse([__DIR__ . '/data/bug-4809.php'], []); + } + + 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 testBugObject(): void + { + $this->analyse([__DIR__ . '/data/bug-object.php'], [ + [ + 'Offset int|object does not exist on array{baz: 21}|array{foo: 17, bar: 19}.', + 12, + ], + ]); + } + + public function testBug3747(): void + { + $this->analyse([__DIR__ . '/data/bug-3747.php'], []); + } + + public function testBug12447(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12447.php'], [ + [ + 'Cannot access an offset on mixed.', + 5, + ], + ]); + } + + public function testBug1061(): void + { + $this->analyse([__DIR__ . '/data/bug-1061.php'], [ + [ + "Offset 'one'|'two' might not exist on array{two: 1, three: 2}.", + 14, + ], + ]); + } + + public function testBug4532(): void + { + $this->analyse([__DIR__ . '/data/bug-4532.php'], [ + [ + 'Offset int|null might not exist on array.', + 25, + ], + ]); + } + + public function testBug10492(): void + { + $this->analyse([__DIR__ . '/data/bug-10492.php'], [ + [ + 'Offset 2|4 might not exist on array{array{0}, array{1}, array{2}}.', + 19, + ], + ]); + } + + public function testBug12926(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12926.php'], []); + } + + public function testBug13538(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-13538.php'], [ + [ + "Offset int might not exist on non-empty-array.", + 13, + ], + [ + "Offset int might not exist on non-empty-array.", + 17, + ], + ]); + } + + public function testBug12805(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12805.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index 624b446d91..1c32d81285 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::createReflectionProvider(), true, false, $this->checkUnions, false, false, false, true); return new OffsetAccessAssignOpRule($ruleLevelHelper); } @@ -37,4 +38,11 @@ public function testRuleWithoutUnions(): void $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/offset-access-assignop-nullsafe.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index 88f38c8ee1..1c78cb3f23 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::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,67 @@ public function testAssignNewOffsetToStubbedClass(): void $this->analyse([__DIR__ . '/data/new-offset-stub.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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 366937c226..4efada6991 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -44,7 +45,33 @@ public function testRule(): void 'ArrayAccess does not accept float.', 38, ], + [ + 'ArrayAccess does not accept int.', + 58, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/offset-access-value-assignment-nullsafe.php'], [ + [ + 'ArrayAccess does not accept int|null.', + 18, + ], ]); } + public function testBug8236(): void + { + $this->analyse([__DIR__ . '/data/bug-8236.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug5655b(): void + { + $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..ce675567f2 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,23 +5,28 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use function usort; /** - * @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(self::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 +43,72 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/unpack-iterable-nullsafe.php'], [ + [ + 'Only iterables can be unpacked, array|null given.', + 17, + ], + ]); + } + + public static 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, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/unpack-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php new file mode 100644 index 0000000000..d8389afe5e --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ArrayDestructuringNullsafe; + +class Foo +{ + + public function doFooBar(?Bar $bar): void + { + [$a] = $bar?->getArray(); + } + +} + +class Bar +{ + + public function getArray(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php index bfe0ff5421..8dd4b625e0 100644 --- a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php @@ -22,4 +22,33 @@ public function doBar(): void ['a' => $a] = ['b' => 1]; } + public function doBaz(): void + { + $arrayObject = new FooArrayObject(); + ['a' => $a] = $arrayObject; + } + +} + +class FooArrayObject implements \ArrayAccess +{ + + public function offsetGet($key) + { + return true; + } + + public function offsetSet($key, $value): void + { + } + + public function offsetUnset($key): void + { + } + + public function offsetExists($key): bool + { + return false; + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/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-10492.php b/tests/PHPStan/Rules/Arrays/data/bug-10492.php new file mode 100644 index 0000000000..cd57c2b804 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10492.php @@ -0,0 +1,20 @@ + 5) { + return '02'; + } + + return '04'; +} + +function (): void { + $arr = [0 => [0], 1 => [1], 2 => [2]]; + echo print_r($arr[(int) getIndex(1)], true); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-1061.php b/tests/PHPStan/Rules/Arrays/data/bug-1061.php new file mode 100644 index 0000000000..2513b4d498 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-1061.php @@ -0,0 +1,15 @@ + 1, + "three" => 2 + ]; +} + +foreach (A::KEYS as $key) { + echo A::ARR[$key]; +} 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 @@ += 8.0 + +namespace Bug12273; + +function doFoo():void { + $map = [ + 'datetime' => \DateTime::class, + 'stdclass' => \stdClass::class, + ]; + + $settings = json_decode('{"class": "datetim"}'); + + \PHPStan\dumpType($map); + \PHPStan\dumpType($settings->class); + + new ($map[$settings->class])(); +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12406.php b/tests/PHPStan/Rules/Arrays/data/bug-12406.php new file mode 100644 index 0000000000..5d967399c1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12406.php @@ -0,0 +1,15 @@ + */ + 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-12447.php b/tests/PHPStan/Rules/Arrays/data/bug-12447.php new file mode 100644 index 0000000000..fb596d1306 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12447.php @@ -0,0 +1,9 @@ + $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-12805.php b/tests/PHPStan/Rules/Arrays/data/bug-12805.php new file mode 100644 index 0000000000..f3b49b6153 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12805.php @@ -0,0 +1,23 @@ + $operations + * @return array + */ +function bug(array $operations): array { + $base = []; + + foreach ($operations as $operationName => $operation) { + if (!isset($base[$operationName])) { + $base[$operationName] = []; + } + if (!isset($base[$operationName]['rtx'])) { + $base[$operationName]['rtx'] = 0; + } + $base[$operationName]['rtx'] += $operation['rtx'] ?? 0; + } + + return $base; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12926.php b/tests/PHPStan/Rules/Arrays/data/bug-12926.php new file mode 100644 index 0000000000..d83bd21b36 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12926.php @@ -0,0 +1,13 @@ +> + */ + public array $mailsGroupedByTemplate; + + /** + * @var array> + */ + protected array $mailCounts; + + private function __construct() + { + $this->mailsGroupedByTemplate = []; + $this->mailCounts = []; + } + + public function countMailStates(): void + { + foreach ($this->mailsGroupedByTemplate as $templateId => $mails) { + $this->mailCounts[$templateId] = [ + MailStatus::notActive()->code => 0, + MailStatus::simulation()->code => 0, + MailStatus::active()->code => 0, + ]; + } + } +} + +final class MailStatus +{ + private const CODE_NOT_ACTIVE = 0; + + private const CODE_SIMULATION = 1; + + private const CODE_ACTIVE = 2; + + /** + * @var self::CODE_* + */ + public int $code; + + public string $name; + + public string $description; + + /** + * @param self::CODE_* $status + */ + public function __construct(int $status, string $name, string $description) + { + $this->code = $status; + $this->name = $name; + $this->description = $description; + } + + public static function notActive(): self + { + return new self(self::CODE_NOT_ACTIVE, _('Pausiert'), _('Es findet kein Mailversand an Kunden statt')); + } + + public static function simulation(): self + { + return new self(self::CODE_SIMULATION, _('Simulation'), _('Wenn Template zugewiesen, werden im Simulationsmodus E-Mails nur in der Datenbank gespeichert und nicht an den Kunden gesendet')); + } + + public static function active(): self + { + return new self(self::CODE_ACTIVE, _('Aktiv'), _('Wenn Template zugewiesen, findet Mailversand an Kunden statt')); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13022.php b/tests/PHPStan/Rules/Arrays/data/bug-13022.php new file mode 100644 index 0000000000..22727c110a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13022.php @@ -0,0 +1,29 @@ + $object->getId(), + $targetId => 'info', + ]; + + // sql()->insert('tablename', $array); - example how this will be used +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13135.php b/tests/PHPStan/Rules/Arrays/data/bug-13135.php new file mode 100644 index 0000000000..750658101b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13135.php @@ -0,0 +1,19 @@ + $iterable + */ +function my_to_array(iterable $iterable): void +{ + $result = []; + foreach ($iterable as $k => $v) { + $result[$k] = $v; + } + + var_dump($result); +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13248.php b/tests/PHPStan/Rules/Arrays/data/bug-13248.php new file mode 100644 index 0000000000..7d67f895eb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13248.php @@ -0,0 +1,37 @@ + + */ +class Y extends X implements IteratorAggregate +{ + /** + * @return ArrayIterator, 'a'|'b'|'c'> + */ + public function getIterator(): Traversable + { + return new ArrayIterator(['a', 'b', 'c']); + } +} + +/** + * @return X&Traversable + */ +function y(): X +{ + return new Y(); +} + +foreach (y() as $item) { // hm? + echo $item . PHP_EOL; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php new file mode 100644 index 0000000000..d1eec2b8d1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -0,0 +1,61 @@ + $arr */ +function doFoo(array $arr, int $i, int $i2): void +{ + $logs = []; + $logs[$i] = ''; + echo $logs[$i2]; + + assertType("non-empty-array", $logs); + assertType("''", $logs[$i]); + assertType("''", $logs[$i2]); // could be mixed + + foreach ($arr as $value) { + echo $logs[$i]; + + assertType("non-empty-array", $logs); + assertType("''", $logs[$i]); + } +} + +/** @param list $arr */ +function doFooBar(array $arr): void +{ + if (!defined('LOG_DIR')) { + throw new LogicException(); + } + + $logs = []; + $logs[LOG_DIR] = ''; + + assertType("non-empty-array<''>", $logs); + assertType("''", $logs[LOG_DIR]); + + foreach ($arr as $value) { + echo $logs[LOG_DIR]; + + assertType("non-empty-array<''>", $logs); + assertType("''", $logs[LOG_DIR]); + } +} + +function doBar(array $arr, int $i, string $s): void +{ + $logs = []; + $logs[$i][$s] = ''; + assertType("non-empty-array>", $logs); + assertType("non-empty-array", $logs[$i]); + assertType("''", $logs[$i][$s]); + foreach ($arr as $value) { + assertType("non-empty-array>", $logs); + assertType("non-empty-array", $logs[$i]); + assertType("''", $logs[$i][$s]); + echo $logs[$i][$s]; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-1714.php b/tests/PHPStan/Rules/Arrays/data/bug-1714.php new file mode 100644 index 0000000000..e237e416bb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-1714.php @@ -0,0 +1,18 @@ + $val, 'text' => $text]; + } + $radio['name'] = $data['name']; + return $radio; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2313.php b/tests/PHPStan/Rules/Arrays/data/bug-2313.php new file mode 100644 index 0000000000..f79d9f0add --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2313.php @@ -0,0 +1,22 @@ + array()); + + safe_inc($data['apples']['count']); + print_r($data); + + safe_inc($data['apples']['count']); + print_r($data); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2457.php b/tests/PHPStan/Rules/Arrays/data/bug-2457.php new file mode 100644 index 0000000000..88811feff4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2457.php @@ -0,0 +1,29 @@ + $x */ + private static array $x; + + public function y(): void { + + self::$x = []; + + $this->z(); + + echo self::$x['foo']; + + } + + private function z(): void { + self::$x['foo'] = 'bar'; + } + +} + +$x = new X(); +$x->y(); diff --git a/tests/PHPStan/Rules/Arrays/data/bug-3872.php b/tests/PHPStan/Rules/Arrays/data/bug-3872.php new file mode 100644 index 0000000000..944cba7757 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3872.php @@ -0,0 +1,19 @@ + '', 'operator' => '']; + if ($item['value']) { + $item['value'] = strtotime($item['value']); + if ($item['operator'] === 'eq') { + echo 'test'; + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4335.php b/tests/PHPStan/Rules/Arrays/data/bug-4335.php new file mode 100644 index 0000000000..0824514d15 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4335.php @@ -0,0 +1,19 @@ + $v) { + var_dump($k, $v); + } + foreach (class_parents($this) as $k => $v) { + var_dump($k, $v); + } + foreach (class_uses($this) as $k => $v) { + var_dump($k, $v); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4532.php b/tests/PHPStan/Rules/Arrays/data/bug-4532.php new file mode 100644 index 0000000000..32f62d98fc --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4532.php @@ -0,0 +1,26 @@ + */ + public static array $statuses = [ + self::STATUS_OFF => 'Off', + self::STATUS_ON => 'On', + ]; +} + +function (): void { + $entity = new Entity; + echo Entity::$statuses[$entity->status]; +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4747.php b/tests/PHPStan/Rules/Arrays/data/bug-4747.php new file mode 100644 index 0000000000..55c55b15e2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4747.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug4885; + +class Foo +{ + /** @param array{word?: string} $data */ + public function sayHello(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + echo 'Again, the word was: ' . $data['word']; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-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-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-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-8236.php b/tests/PHPStan/Rules/Arrays/data/bug-8236.php new file mode 100644 index 0000000000..aae201a891 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8236.php @@ -0,0 +1,18 @@ +'); + $string = 'But isn\'t 7 > 5 & 9 < 11?'; + $node = $xml->a->addChild('formula1'); + if ($node !== null) { + $node[0] = $string; + } + echo $xml->asXML(); + } +} +HelloWorld::sayHello(); 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 @@ + $array + * @param object $key + */ + public function foo2($array, $key) + { + $array[$key]; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php index f0ddf61b94..8e7725d5b8 100644 --- a/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php +++ b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php @@ -57,4 +57,130 @@ public function doIncrement2() ]; } + public function doWithoutKeys(int $int) + { + $foo = [ + 1, // Key is 0 + 0 => 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', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function maybeDuplicate(string $key): void + { + $a = [ + 'foo' => 'foo', + $key => 'foo|bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate(string $key): void + { + $a = [ + 'foo' => 'foo', + $key => 'foo|bar', + 'bar' => 'bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate2(string $key): void + { + $a = [ + $key => 'foo|bar', + 'foo' => 'foo', + 'bar' => 'bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate3(string $key): void + { + $a = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar', + ]; + } + + /** + * @param 'foo'|'bar'|'baz' $key + */ + public function sureDuplicate4(string $key): void + { + $a = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar|baz', + ]; + + $b = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar|baz', + 'baz' => 'baz', + ]; + } + + public function duplicateWithCast(): void + { + $a = [ + 1 => 'foo', + '1' => 'bar', + true => 'baz', + 1.0 => 'some', + 1.1 => 'thing' + ]; + } } diff --git a/tests/PHPStan/Rules/Arrays/data/empty-array-item.php b/tests/PHPStan/Rules/Arrays/data/empty-array-item.php deleted file mode 100644 index 4a08a799a8..0000000000 --- a/tests/PHPStan/Rules/Arrays/data/empty-array-item.php +++ /dev/null @@ -1,7 +0,0 @@ -= 8.0 + +namespace 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 + */ diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php new file mode 100644 index 0000000000..8185c01f8f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php @@ -0,0 +1,19 @@ += 8.0 + +namespace NonexistentOffsetNullsafe; + +class Foo +{ + + /** @var array{a: int} */ + public array $array = [ + 'a' => 1, + ]; + +} + +function nonexistentOffsetOnArray(?Foo $foo): void +{ + echo $foo?->array['a']; + echo $foo?->array[1]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php index db4335e930..4509a69a2b 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php @@ -459,3 +459,62 @@ public function foo(array $array): int return 0; } } + +class MessageDescriptorTest +{ + + public function testDefinitions(): void + { + try { + doFoo(); + } catch (\TypeError $e) { + $trace = $e->getTrace(); + if (isset($trace[1]['args'][0])) { + $class = $trace[1]['args'][0]; + $this->fail(sprintf('Invalid phpDoc in class: %s', $class)); + } + + throw $e; + } + } + + /** @param array|null $array */ + function test($array): void { + var_dump($array['test1']['test2'] ?? true); + var_dump($array['test1'] ?? true); + } + +} + +/** + * @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} + */ +class VersionGuesser +{ + /** + * @param array $versionData + * + * @phpstan-param Version $versionData + * + * @return array + * @phpstan-return Version + */ + private function postprocess(array $versionData): array + { + if (!empty($versionData['feature_version']) && $versionData['feature_version'] === $versionData['version'] && $versionData['feature_pretty_version'] === $versionData['pretty_version']) { + unset($versionData['feature_version'], $versionData['feature_pretty_version']); + } + + return $versionData; + } +} + +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 9ce7827d02..2e4f00d7e3 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php @@ -44,3 +44,18 @@ public function doLorem(string $str): void } } + +class AppendToArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/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 81621e9f19..4072e82b0d 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::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, ], [ @@ -43,9 +44,24 @@ public function testEchoRule(): void 13, ], [ - 'Parameter #1 (\'string\'|array(\'string\')) of echo cannot be converted to string.', + 'Parameter #1 (\'string\'|array{\'string\'}) of echo cannot be converted to string.', 17, ], + [ + 'Parameter #1 (array{}) of echo cannot be converted to string.', + 29, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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 8794b9b6d9..3bbc8521a5 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -2,18 +2,28 @@ namespace PHPStan\Rules\Cast; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use function usort; /** - * @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)); + $broker = self::createReflectionProvider(); + return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testRule(): void @@ -31,13 +41,17 @@ public function testRule(): void 'Cannot cast stdClass to float.', 24, ], + [ + 'Cannot cast object to string.', + 36, + ], [ 'Cannot cast Test\\Foo to string.', - 41, + 42, ], [ 'Cannot cast array|float|int to string.', - 48, + 49, ], ]); } @@ -47,4 +61,110 @@ public function testBug5162(): void $this->analyse([__DIR__ . '/data/bug-5162.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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 static 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, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkImplicitMixed = $checkImplicitMixed; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/mixed-cast.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 0429ec48ee..b42c44cd09 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -28,4 +33,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/invalid-encapsed-part-nullsafe.php'], [ + [ + 'Part $bar?->obj (stdClass|null) of encapsed string cannot be cast to string.', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index fa12ad4424..509ab75570 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::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, ], [ @@ -35,7 +36,7 @@ public function testPrintRule(): void 9, ], [ - 'Parameter array() of print cannot be converted to string.', + 'Parameter array{} of print cannot be converted to string.', 13, ], [ @@ -47,10 +48,21 @@ public function testPrintRule(): void 17, ], [ - 'Parameter \'string\'|array(\'string\') of print cannot be converted to string.', + 'Parameter \'string\'|array{\'string\'} of print cannot be converted to string.', 21, ], ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/print-nullsafe.php'], [ + [ + 'Parameter array|null of print cannot be converted to string.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php index d9000d4378..db6263ec9d 100644 --- a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -12,15 +13,14 @@ class UnsetCastRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { return new UnsetCastRule(new PhpVersion($this->phpVersion)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -40,10 +40,9 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * @param int $phpVersion - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $errors): void { $this->phpVersion = $phpVersion; diff --git a/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php b/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php new file mode 100644 index 0000000000..f9143c3ca7 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php @@ -0,0 +1,37 @@ + + */ +class VoidCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VoidCastRule(); + } + + public function testPrintRule(): void + { + $this->analyse([__DIR__ . '/data/void-cast.php'], [ + [ + 'The (void) cast cannot be used within an expression.', + 5, + ], + [ + 'The (void) cast cannot be used within an expression.', + 6, + ], + [ + 'The (void) cast cannot be used within an expression.', + 7, + ], + ]); + } + +} 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 4c48e6acfa..061f41342f 100644 --- a/tests/PHPStan/Rules/Cast/data/invalid-cast.php +++ b/tests/PHPStan/Rules/Cast/data/invalid-cast.php @@ -25,6 +25,7 @@ function ( (string) fopen('php://memory', 'r'); (int) fopen('php://memory', 'r'); + (float) fopen('php://memory', 'r'); }; function ( 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/void-cast.php b/tests/PHPStan/Rules/Cast/data/void-cast.php new file mode 100644 index 0000000000..7c8c342055 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/void-cast.php @@ -0,0 +1,7 @@ + + */ +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 function testSealed(): void + { + $this->analyse([__DIR__ . '/data/sealed.php'], [ + [ + 'Type Sealed\BazClass is not allowed to be a subtype of Sealed\BaseClass.', + 11, + ], + [ + 'Type Sealed\BazClass2 is not allowed to be a subtype of Sealed\BaseInterface.', + 19, + ], + [ + 'Type Sealed\BazInterface is not allowed to be a subtype of Sealed\BaseInterface2.', + 27, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + __DIR__ . '/data/allowed-sub-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 02894dedbd..c85ac4f704 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,15 +2,18 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -18,33 +21,40 @@ class ClassAttributesRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ClassAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ), ); } + #[RequiresPhp('>= 8.0')] public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/class-attributes.php'], [ [ 'Attribute class ClassAttributes\Nonexistent does not exist.', @@ -94,6 +104,84 @@ public function testRule(): void 'Unknown parameter $r in call to ClassAttributes\AttributeWithConstructor constructor.', 120, ], + [ + 'Interface ClassAttributes\InterfaceAsAttribute is not an Attribute class.', + 132, + ], + [ + 'Trait ClassAttributes\TraitAsAttribute is not an Attribute class.', + 142, + ], + [ + 'Attribute class ClassAttributes\FlagsAttributeWithPropertyTarget does not have the class target.', + 164, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleForEnums(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testAllowDynamicPropertiesAttribute(): void + { + $this->analyse([__DIR__ . '/data/allow-dynamic-properties-attribute.php'], []); + } + + #[RequiresPhp('>= 8.3')] + public function testBug12011(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testBug12281(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with readonly class.', + 05, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with enum.', + 12, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with interface.', + 15, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index b233afdb49..a2a1f5328b 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class ClassConstantAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ClassConstantAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/class-constant-attributes.php'], [ [ 'Attribute class ClassConstantAttributes\Foo does not have the class constant target.', diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 53a1da938e..9d24119731 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -4,22 +4,38 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ClassConstantRuleTest extends \PHPStan\Testing\RuleTestCase +class ClassConstantRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersion)); + $reflectionProvider = self::createReflectionProvider(); + return new ClassConstantRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new PhpVersion($this->phpVersion), + true, + ); } public function testClassConstant(): void @@ -44,6 +60,10 @@ public function testClassConstant(): void 'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.', 10, ], + [ + 'Cannot access constant LOREM on mixed.', + 11, + ], [ 'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.', 16, @@ -81,16 +101,12 @@ 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'], [ [ @@ -180,7 +196,7 @@ public function testClassExists(): void ]); } - public function dataClassConstantOnExpression(): array + public static function dataClassConstantOnExpression(): array { return [ [ @@ -202,6 +218,10 @@ public function dataClassConstantOnExpression(): array 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', 18, ], + [ + 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', + 19, + ], ], ], [ @@ -225,25 +245,17 @@ public function dataClassConstantOnExpression(): array } /** - * @dataProvider dataClassConstantOnExpression - * @param int $phpVersion - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataClassConstantOnExpression')] public function testClassConstantOnExpression(int $phpVersion, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->phpVersion = $phpVersion; $this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors); } public function testAttributes(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/class-constant-attribute.php'], [ [ @@ -277,4 +289,259 @@ public function testAttributes(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testClassConstantAccessedOnTrait(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-constant-accessed-on-trait.php'], [ + [ + 'Cannot access constant TEST on trait ClassConstantAccessedOnTrait\Foo.', + 16, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testDynamicAccess(): void + { + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [ + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 17, + ], + [ + 'Class constant name for ClassConstantDynamicAccess\Foo must be a string, but object was given.', + 19, + ], + [ + '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, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testStringableDynamicAccess(): void + { + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [ + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.', + 14, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but string|null was given.', + 15, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable|null was given.', + 16, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int was given.', + 17, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int|null was given.', + 18, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but DateTime|string was given.', + 19, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but 1111 was given.', + 20, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable was given.', + 22, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.', + 32, + ], + [ + 'Class constant name for ClassConstantDynamicStringableAccess\Bar must be a string, but mixed was given.', + 33, + ], + [ + 'Class constant name for DateTime|DateTimeImmutable must be a string, but mixed was given.', + 38, + ], + [ + 'Class constant name for object must be a string, but mixed was given.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php new file mode 100644 index 0000000000..e16ccce93c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php @@ -0,0 +1,52 @@ + + */ +class DuplicateClassDeclarationRuleTest extends RuleTestCase +{ + + private const FILENAME = __DIR__ . '/data/duplicate-class.php'; + + protected function getRule(): Rule + { + $fileHelper = new FileHelper(__DIR__ . '/data'); + + return new DuplicateClassDeclarationRule( + new DefaultReflector(new OptimizedSingleFileSourceLocator( + self::getContainer()->getByType(FileNodesFetcher::class), + self::FILENAME, + )), + new SimpleRelativePathHelper($fileHelper->normalizePath($fileHelper->getWorkingDirectory(), '/')), + ); + } + + public function testRule(): void + { + $this->analyse([self::FILENAME], [ + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:15\n- duplicate-class.php:20", + 10, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:20", + 15, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:15", + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php index 5ea6be4bd8..c0f21a6053 100644 --- a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase<\PHPStan\Rules\Classes\DuplicateDeclarationRule> + * @extends RuleTestCase */ class DuplicateDeclarationRuleTest extends RuleTestCase { @@ -18,10 +19,6 @@ protected function getRule(): Rule public function testDuplicateDeclarations(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse( [ __DIR__ . '/data/duplicate-declarations.php', @@ -51,16 +48,12 @@ public function testDuplicateDeclarations(): void 'Cannot redeclare method DuplicateDeclarations\Foo::Func1().', 35, ], - ] + ], ); } public function testDuplicatePromotedProperty(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/duplicate-promoted-property.php'], [ [ 'Cannot redeclare property DuplicatedPromotedProperty\Foo::$foo.', @@ -73,4 +66,23 @@ public function testDuplicatePromotedProperty(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testDuplicateEnumCase(): void + { + $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..22bb10a00e --- /dev/null +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -0,0 +1,144 @@ + + */ +class EnumSanityRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumSanityRule(); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9402(): void + { + $this->analyse([__DIR__ . '/data/bug-9402.php'], [ + [ + 'Enum case Bug9402\Foo::Two value \'foo\' does not match the "int" type.', + 13, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11592(): void + { + $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 f429d082dd..4ae78ffe28 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInClassExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInClassExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -36,10 +46,6 @@ public function testRule(): void public function testRuleExtendsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/extends-error.php'], [ [ 'Class ExtendsError\Foo extends unknown class ExtendsError\Bar.', @@ -75,4 +81,75 @@ public function testFinalByTag(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testReadonly(): void + { + $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 18d571f3e8..2045a7d496 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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 = self::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,7 +47,7 @@ public function testClassDoesNotExist(): void ], [ [ - 'Class InstanceOfNamespace\Bar not found.', + 'Class InstanceOfNamespaceRule\Bar not found.', 7, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -39,7 +56,7 @@ public function testClassDoesNotExist(): void 9, ], [ - 'Class InstanceOfNamespace\Foo referenced with incorrect case: InstanceOfNamespace\FOO.', + 'Class InstanceOfNamespaceRule\Foo referenced with incorrect case: InstanceOfNamespaceRule\FOO.', 13, ], [ @@ -50,7 +67,7 @@ public function testClassDoesNotExist(): void 'Using self outside of class scope.', 17, ], - ] + ], ); } @@ -59,4 +76,38 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/instanceof-class-exists.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug7720(): void + { + $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 826094c52a..a605490cfc 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInTraitUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInTraitUseRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,10 +42,6 @@ public function testClassWithWrongCase(): void public function testTraitUseError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/trait-use-error.php'], [ [ 'Class TraitUseError\Foo uses unknown trait TraitUseError\FooTrait.', @@ -66,4 +72,19 @@ public function testTraitUseError(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $this->analyse([__DIR__ . '/data/trait-use-enum.php'], [ + [ + 'Class TraitUseEnum\Foo uses enum TraitUseEnum\FooEnum.', + 13, + ], + [ + 'Anonymous class uses enum TraitUseEnum\FooEnum.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 72f58b2369..c3aea0595c 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClassImplementsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClassImplementsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,10 +42,6 @@ public function testRule(): void public function testRuleImplementsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/implements-error.php'], [ [ 'Class ImplementsError\Foo implements unknown interface ImplementsError\Bar.', @@ -57,4 +63,35 @@ public function testRuleImplementsError(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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..98bf1f3608 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -0,0 +1,70 @@ + + */ +class ExistingClassesInEnumImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new ExistingClassesInEnumImplementsRule( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/enum-implements.php'], [ + [ + 'Interface EnumImplements\FooInterface referenced with incorrect case: EnumImplements\FOOInterface.', + 30, + ], + [ + 'Enum EnumImplements\Foo3 implements class EnumImplements\FooClass.', + 35, + ], + [ + 'Enum EnumImplements\Foo4 implements trait EnumImplements\FooTrait.', + 40, + ], + [ + 'Enum EnumImplements\Foo5 implements enum EnumImplements\FooEnum.', + 45, + ], + [ + 'Enum EnumImplements\Foo6 implements unknown interface EnumImplements\NonexistentInterface.', + 50, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Enum EnumImplements\FooEnum referenced with incorrect case: EnumImplements\FOOEnum.', + 55, + ], + [ + 'Enum EnumImplements\Foo7 implements enum EnumImplements\FooEnum.', + 55, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index 196edc0421..18ecc90324 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInInterfaceExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,10 +42,6 @@ public function testRule(): void public function testRuleExtendsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/interface-extends-error.php'], [ [ 'Interface InterfaceExtendsError\Foo extends unknown interface InterfaceExtendsError\Bar.', @@ -53,4 +59,15 @@ public function testRuleExtendsError(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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..cf4d172f92 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -0,0 +1,64 @@ + + */ +class ForbiddenNameCheckExtensionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new InstantiationRule( + self::getContainer(), + $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(), + ), + new ConsistentConstructorHelper(), + 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 8f6c11818f..5fb37f5340 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -2,21 +2,34 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function sprintf; +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); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true); + + return new ImpossibleInstanceOfRule( + $ruleLevelHelper, + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -26,7 +39,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 +73,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 +130,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.', @@ -167,117 +178,24 @@ public function testInstanceof(): void [ 'Instanceof between class-string and class-string will always evaluate to false.', 419, + $tipText, ], [ - 'Instanceof between class-string and string will always evaluate to false.', + 'Instanceof between class-string and \'DateTimeInterface\' will always evaluate to false.', 432, $tipText, ], [ - 'Instanceof between DateTimeInterface and string will always evaluate to true.', + 'Instanceof between DateTimeInterface and \'DateTimeInterface\' will always evaluate to true.', 433, $tipText, ], - ] - ); - } - - 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 DateTimeInterface will always evaluate to false.', - 418, - $tipText, - ], - [ - 'Instanceof between class-string and class-string will always evaluate to false.', - 419, - ], - [ - 'Instanceof between class-string and string will always evaluate to false.', - 432, - $tipText, - ], - ] + ], ); } public function testDoNotReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ @@ -289,11 +207,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, ], ]); @@ -301,7 +219,6 @@ public function testDoNotReportTypesFromPhpDocs(): void public function testReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ @@ -335,9 +252,321 @@ public function testReportTypesFromPhpDocs(): void public function testBug3096(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3096.php'], []); } + public function testBug6213(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6213.php'], []); + } + + public function testBug5333(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-5333.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8042(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7721(): void + { + $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 static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10201(): void + { + $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 testBug10036(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10036.php'], [ + [ + 'Instanceof between stdClass and string|null results in an error.', + 11, + ], + [ + 'Instanceof between stdClass and string|null results in an error.', + 19, + ], + [ + 'Instanceof between stdClass and array results in an error.', + 39, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testNewIsAlwaysFinalClass(): void + { + $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, + ], + ]); + } + + public function testBug13469(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13469.php'], [ + [ + sprintf('Instanceof between Bug13469\Foo and Stringable will always evaluate to %s.', PHP_VERSION_ID >= 80000 ? 'true' : 'false'), + 23, + ], + ]); + } + } 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 c9f7a241b6..945f08e533 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,34 +2,44 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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 = self::createReflectionProvider(); return new InstantiationRule( - $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), true, true, true, true), - new ClassCaseSensitivityCheck($broker) + self::getContainer(), + $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(), + ), + new ConsistentConstructorHelper(), + 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'], [ @@ -190,7 +200,7 @@ public function testInstantiation(): void 'Class TestInstantiation\ClassExtendingAbstractConstructor constructor invoked with 0 parameters, 1 required.', 273, ], - ] + ], ); } @@ -203,7 +213,7 @@ public function testSoap(): void 'Parameter #2 $string of class SoapFault constructor expects string, int given.', 6, ], - ] + ], ); } @@ -222,12 +232,9 @@ public function testBug3404(): void ]); } + #[RequiresPhp('>= 8.0')] 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.', @@ -240,27 +247,15 @@ public function testOldStyleConstructorOnPhp8(): void ]); } + #[RequiresPhp('< 8.0')] public function testOldStyleConstructorOnPhp7(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP 7.x'); - } - - $errors = [ + $this->analyse([__DIR__ . '/data/php80-constructor.php'], [ [ 'Class OldStyleConstructorOnPhp8 constructor invoked with 0 parameters, 1 required.', 19, ], - ]; - - if (!self::$useStaticReflectionProvider) { - $errors[] = [ - 'Methods with the same name as their class will not be constructors in a future version of PHP; OldStyleConstructorOnPhp8 has a deprecated constructor', - 3, - ]; - } - - $this->analyse([__DIR__ . '/data/php80-constructor.php'], $errors); + ]); } public function testBug4030(): void @@ -270,10 +265,6 @@ public function testBug4030(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/instantiation-promoted-properties.php'], [ [ 'Parameter #2 $bar of class InstantiationPromotedProperties\Foo constructor expects array, array given.', @@ -283,6 +274,10 @@ public function testPromotedProperties(): void 'Parameter #2 $bar of class InstantiationPromotedProperties\Bar constructor expects array, array given.', 33, ], + [ + 'Parameter #1 $intProp of class InstantiationPromotedProperties\PromotedPropertyNotNullable constructor expects int, null given.', + 46, + ], ]); } @@ -291,12 +286,9 @@ public function testBug4056(): void $this->analyse([__DIR__ . '/data/bug-4056.php'], []); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $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.', @@ -350,11 +342,239 @@ public function testBug5002(): void public function testBug4681(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-4681.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testFirstClassCallable(): void + { + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-instantiation-callable.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testEnumInstantiation(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug5553(): void + { + $this->analyse([__DIR__ . '/data/bug-5553.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7048(): void + { + $this->analyse([__DIR__ . '/data/bug-7048.php'], [ + [ + 'Unknown parameter $recurrences in call to DatePeriod constructor.', + 21, + ], + [ + 'Missing parameter $end (int|TEnd of DateTimeInterface) 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 int|TEnd of DateTimeInterface, string given.', + 41, + ], + [ + 'Parameter $end of class DatePeriod constructor expects int|TEnd of DateTimeInterface, string given.', + 49, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7594(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11815(): void + { + $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, + ], + ]); + } + + public function testInternalConstructor(): void + { + $this->analyse([__DIR__ . '/data/internal-constructor.php'], [ + [ + 'Call to internal method InternalConstructorDefinition\Foo::__construct() from outside its root namespace InternalConstructorDefinition.', + 21, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12951(): void + { + require_once __DIR__ . '/../InternalTag/data/bug-12951-define.php'; + $this->analyse([__DIR__ . '/../InternalTag/data/bug-12951-constructor.php'], [ + [ + 'Instantiation of internal class Bug12951Polyfill\NumberFormatter.', + 7, + ], + [ + 'Call to method __construct() of internal class Bug12951Polyfill\NumberFormatter from outside its root namespace Bug12951Polyfill.', + 7, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArgumentsPhpversion(): void + { + $this->analyse([__DIR__ . '/data/named-arguments-phpversion.php'], []); + } + + public function testNewStaticWithConsistentConstructor(): void + { + $this->analyse([__DIR__ . '/data/instantiation-new-static-consistent-constructor.php'], [ + [ + 'Parameter #1 $i of class InstantiationNewStaticConsistentConstructor\Foo constructor expects int, string given.', + 18, + ], + [ + 'Parameter #1 $value of class InstantiationNewStaticConsistentConstructor\ChildClass3 constructor expects string, int given.', + 38, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index 9023c74faa..f58e3fa475 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -12,8 +13,7 @@ class InvalidPromotedPropertiesRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { @@ -22,9 +22,6 @@ protected function getRule(): Rule public function testNotSupportedOnPhp7(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersion = 70400; $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ [ @@ -64,9 +61,6 @@ public function testNotSupportedOnPhp7(): void public function testSupportedOnPhp8(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ [ @@ -100,4 +94,23 @@ public function testSupportedOnPhp8(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug9577(): void + { + $this->phpVersion = 80100; + $this->analyse([__DIR__ . '/data/bug-9577.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testHooks(): void + { + $this->phpVersion = 80100; + $this->analyse([__DIR__ . '/data/invalid-hooked-properties.php'], [ + [ + 'Promoted properties can be in constructor only.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 6353bb0e57..b08d3804bf 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -3,8 +3,15 @@ namespace PHPStan\Rules\Classes; use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,10 +21,26 @@ class LocalTypeAliasesRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = self::createReflectionProvider(); + return new LocalTypeAliasesRule( - ['GlobalTypeAlias' => 'int|string'], - $this->createReflectionProvider(), - self::getContainer()->getByType(TypeNodeResolver::class) + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + self::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, + ), ); } @@ -88,6 +111,51 @@ public function testRule(): void 'Invalid type definition detected in type alias InvalidTypeAlias.', 62, ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoIterableValue with no value type specified in iterable type array.', + 77, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoGenerics with generic class LocalTypeAliases\Generic but does not specify its types: T', + 77, + ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoCallable with no signature specified for callable.', + 77, + ], + [ + 'Type alias A contains unknown class LocalTypeAliases\Nonexistent.', + 87, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Type alias B contains invalid type LocalTypeTraitAliases\Foo.', + 87, + ], + [ + 'Class LocalTypeAliases\Foo referenced with incorrect case: LocalTypeAliases\fOO.', + 87, + ], + [ + 'Type alias A contains unresolvable type.', + 95, + ], + [ + 'Type alias A contains generic type Exception but class Exception is not generic.', + 103, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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..bd93fd00a2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php @@ -0,0 +1,127 @@ + + */ +class LocalTypeTraitAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new LocalTypeTraitAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + self::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, + ), + self::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..d2c40ea875 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php @@ -0,0 +1,80 @@ + + */ +class LocalTypeTraitUseAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new LocalTypeTraitUseAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + self::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..3587a486ac --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagRuleTest.php @@ -0,0 +1,110 @@ + + */ +class MethodTagRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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..2581708333 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php @@ -0,0 +1,60 @@ + + */ +class MethodTagTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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..d2c390632a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php @@ -0,0 +1,81 @@ + + */ +class MethodTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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 c94714b072..7cbd36cda4 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -3,12 +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 PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -18,16 +20,24 @@ class MixinRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new MixinRule( - self::getContainer()->getByType(FileTypeMapper::class), - $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider), - new GenericObjectTypeCheck(), - new MissingTypehintCheck($reflectionProvider, true, true, true), - new UnresolvableTypeHelper(), - true + new MixinCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), ); } @@ -47,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, ], [ @@ -57,7 +67,6 @@ public function testRule(): void [ 'PHPDoc tag @mixin contains generic class ReflectionClass but does not specify its types: T', 50, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'PHPDoc tag @mixin contains unknown class MixinRule\UnknownestClass.', @@ -81,6 +90,39 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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..0c330061d8 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MixinTraitRuleTest.php @@ -0,0 +1,55 @@ + + */ +class MixinTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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..23108e7e09 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MixinTraitUseRuleTest.php @@ -0,0 +1,53 @@ + + */ +class MixinTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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/NewStaticInAbstractClassStaticMethodRuleTest.php b/tests/PHPStan/Rules/Classes/NewStaticInAbstractClassStaticMethodRuleTest.php new file mode 100644 index 0000000000..2212bafe42 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/NewStaticInAbstractClassStaticMethodRuleTest.php @@ -0,0 +1,30 @@ + + */ +class NewStaticInAbstractClassStaticMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(NewStaticInAbstractClassStaticMethodRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/new-static-in-abstract-class-static-method.php'], [ + [ + 'Unsafe usage of new static() in abstract class NewStaticInAbstractClassStaticMethod\Bar in static method staticDoFoo().', + 30, + 'Direct call to NewStaticInAbstractClassStaticMethod\Bar::staticDoFoo() would crash because an abstract class cannot be instantiated.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php index 785bdfcfe7..b096e8119b 100644 --- a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php @@ -2,17 +2,23 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NewStaticRuleTest extends \PHPStan\Testing\RuleTestCase +class NewStaticRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new NewStaticRule(); + return new NewStaticRule( + new PhpVersion(PHP_VERSION_ID), + new ConsistentConstructorHelper(), + ); } public function testRule(): void @@ -33,4 +39,38 @@ public function testRule(): void ]); } + public function testRuleWithConsistentConstructor(): void + { + $this->analyse([__DIR__ . '/data/new-static-consistent-constructor.php'], []); + } + + public function testBug9654(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + 'Unsafe usage of new static().', + 11, + 'See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static', + ]; + $errors[] = [ + 'Unsafe usage of new static().', + 11, + 'See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static', + ]; + } + + $this->analyse([__DIR__ . '/data/bug-9654.php'], $errors); + } + + public function testBug11316(): void + { + $this->analyse([__DIR__ . '/data/bug-11316.php'], []); + } + + public function testBug10722(): void + { + $this->analyse([__DIR__ . '/data/bug-10722.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php index b882278baa..1b7901e273 100644 --- a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -18,9 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ [ 'Interface cannot be an Attribute class.', @@ -41,4 +39,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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..c78a1ab297 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagRuleTest.php @@ -0,0 +1,143 @@ + + */ +class PropertyTagRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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..df9bfe5254 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php @@ -0,0 +1,60 @@ + + */ +class PropertyTagTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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..cdce3b7f87 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php @@ -0,0 +1,59 @@ + + */ +class PropertyTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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..966fac799a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php @@ -0,0 +1,93 @@ + + */ +class RequireExtendsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireExtendsRule(); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..41d2668a4b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php @@ -0,0 +1,83 @@ + + */ +class RequireImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsRule(); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $expectedErrors = [ + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse2 does not.', + 47, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InvalidEnumTraitUse does not.', + 52, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse does not.', + 56, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:117 does not.', + 117, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait1 requires using class to implement IncompatibleRequireImplements\SomeTrait, but IncompatibleRequireImplements\InvalidTraitUse1 does not.', + 125, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait2 requires using class to implement IncompatibleRequireImplements\SomeEnum, but IncompatibleRequireImplements\InvalidTraitUse2 does not.', + 129, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait3 requires using class to implement IncompatibleRequireImplements\TypeDoesNotExist, but IncompatibleRequireImplements\InvalidTraitUse3 does not.', + 133, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait4 requires using class to implement IncompatibleRequireImplements\SomeClass, but IncompatibleRequireImplements\InvalidTraitUse4 does not.', + 137, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but IncompatibleRequireImplements\RequiredInterface@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:164 does not.', + 164, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + ]; + + $this->analyse([__DIR__ . '/../PhpDoc/data/incompatible-require-implements.php'], $expectedErrors); + } + + public function testExtendedTraitBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-implements-trait.php'], [ + [ + 'Trait Bug10302ExtendedImplementsTrait\Foo requires using class to implement Bug10302ExtendedImplementsTrait\Interface1, but Bug10302ExtendedImplementsTrait\Baz does not.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php index fc08dcfe74..e22377bd1c 100644 --- a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php @@ -18,9 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ [ 'Trait cannot be an Attribute class.', diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index 233a39d859..3550be5799 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -2,23 +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( - $this->createReflectionProvider() + self::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.', @@ -31,12 +37,22 @@ public function testUnusedConstructorParameters(): void ]); } - public function testPromotedProperties(): void + public function testUnusedConstructorParameters(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } + $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', + 19, + ], + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $anotherUnusedParameter.', + 20, + ], + ]); + } + public function testPromotedProperties(): void + { $this->analyse([__DIR__ . '/data/unused-constructor-parameters-promoted-properties.php'], []); } @@ -45,4 +61,19 @@ public function testBug1917(): void $this->analyse([__DIR__ . '/data/bug-1917.php'], []); } + public function testBug7165(): void + { + $this->analyse([__DIR__ . '/data/bug-7165.php'], []); + } + + public function testBug10865(): void + { + $this->analyse([__DIR__ . '/data/bug-10865.php'], []); + } + + public function testBug11454(): void + { + $this->analyse([__DIR__ . '/data/bug-11454.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php b/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php new file mode 100644 index 0000000000..e17ad37e5b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php @@ -0,0 +1,9 @@ +getName() === 'AllowedSubTypes\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypes\\Bar'), + ]; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-10036.php b/tests/PHPStan/Rules/Classes/data/bug-10036.php new file mode 100644 index 0000000000..e2894e9a1d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10036.php @@ -0,0 +1,41 @@ +=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 @@ += 8.0 + +namespace Bug10722; + +class BaseClass { + public function __construct(protected string $value) { + } +} + +/** + * @phpstan-consistent-constructor + */ +class ChildClass extends BaseClass { + public function fromString(string $value): static { + return new static($value); + } +} + +/** + * @phpstan-consistent-constructor + */ +class ChildClass2 extends BaseClass { + +} + +class ChildClass3 extends ChildClass2 { + public function fromString(string $value): static { + return new static($value); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-10865.php b/tests/PHPStan/Rules/Classes/data/bug-10865.php new file mode 100644 index 0000000000..7fd8f0d04a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10865.php @@ -0,0 +1,21 @@ + $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-11316.php b/tests/PHPStan/Rules/Classes/data/bug-11316.php new file mode 100644 index 0000000000..d46b93d716 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11316.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug11316; + +/** @phpstan-consistent-constructor */ +class Model +{ + public static function create(): static + { + return new static(); + } +} + +class ParentWithoutConstructor +{ + +} + +/** @phpstan-consistent-constructor */ +class ChildExtendingParentWithoutConstructor extends ParentWithoutConstructor +{ + + public static function create(): static + { + return new static(); + } + +} 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-13469.php b/tests/PHPStan/Rules/Classes/data/bug-13469.php new file mode 100644 index 0000000000..89f73388ea --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-13469.php @@ -0,0 +1,25 @@ + + * @psalm-var list + */ + public array $bar = []; + + /** + * @param array $bar + * @psalm-param list $bar + */ + public function __construct(array $bar) + { + $this->bar = $bar; + } +} + +function () { + $instance = new Foo([1 => 'baz']); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-3632.php b/tests/PHPStan/Rules/Classes/data/bug-3632.php new file mode 100644 index 0000000000..d21950cd6c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3632.php @@ -0,0 +1,41 @@ +getReservedKeywordsClass(); + $keywords = new $class(); + if (! $keywords instanceof KeywordList) { + throw new \Exception(); + } + + return $keywords; + } + + /** + * @throws \Exception If not supported on this platform. + * + * @psalm-return class-string + */ + protected function getReservedKeywordsClass(): string + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-5333.php b/tests/PHPStan/Rules/Classes/data/bug-5333.php new file mode 100644 index 0000000000..67845acd46 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5333.php @@ -0,0 +1,122 @@ +', $foo); + assertNativeType('array', $foo); + + assertType('Bug5333\Route', $res); + assertNativeType('Bug5333\Route', $res); + + return $res; + } + + return $foo; + } +} + +class HelloWorld2 +{ + /** + * @var Route|callable():Route + **/ + private $foo; + + /** + * @param Route|callable():Route $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): Route + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('(Bug5333\Route&callable(): mixed)|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('mixed', $res); + assertNativeType('mixed', $res); + if (!$res instanceof Route) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} + +class HelloFinalWorld +{ + /** + * @var FinalRoute|callable():FinalRoute + **/ + private $foo; + + /** + * @param FinalRoute|callable():FinalRoute $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): FinalRoute + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('callable(): Bug5333\FinalRoute', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('Bug5333\FinalRoute', $res); + assertNativeType('mixed', $res); + if (!$res instanceof FinalRoute) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-5553.php b/tests/PHPStan/Rules/Classes/data/bug-5553.php new file mode 100644 index 0000000000..a1f9d45a0a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5553.php @@ -0,0 +1,29 @@ += 8.0 + +namespace Bug5553; + +use DatePeriod; +use DateTime; + +class Foo +{ + + public function weekNumberToWorkRange(int $week, int $year): DatePeriod + { + $dto = new DateTime(); + $dto->setISODate($year, $week); + $dto->setTime(8, 0, 0, 0); + + $start = clone $dto; + $dto->modify('+4 days'); + $end = clone $dto; + $end->setTime(18, 0, 0, 0); + + return new DatePeriod( + start: $start, + interval: new \DateInterval('P1D'), + end: $end + ); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-6213.php b/tests/PHPStan/Rules/Classes/data/bug-6213.php new file mode 100644 index 0000000000..1e6141f62e --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-6213.php @@ -0,0 +1,21 @@ +createElement('div', 'content'); + + // Incorrect! This is DOMNode|null not DOMElement|null + // It's also possible to contain a DOMText node, which is not an instance + // of DOMElement, but an instance of DOMNode! + if ($element->firstChild instanceof DOMText) { + // do something + } + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-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-9654.php b/tests/PHPStan/Rules/Classes/data/bug-9654.php new file mode 100644 index 0000000000..3b78a8e1b3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9654.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug9654; + +trait Abc +{ + abstract public function __construct(); + + public static function create(): static + { + return new static(); // this is safe as the constructor is defined abstract in this trait + } +} + +class Foo +{ + use Abc; + + public function __construct() { + + } +} + +class Bar +{ + use Abc; + + public function __construct(int $i = 0) + { + $i = $i*2; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9659.php b/tests/PHPStan/Rules/Classes/data/bug-9659.php new file mode 100644 index 0000000000..b78128fdca --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9659.php @@ -0,0 +1,17 @@ +=8.0 + +namespace Bug9659; + +class HelloWorld +{ + /** + * @param float|null $timeout + */ + public function __construct($timeout = null) + { + var_dump($timeout); + } +} + +new HelloWorld(20); // working +new HelloWorld(random_int(20, 80)); // broken diff --git a/tests/PHPStan/Rules/Classes/data/bug-9946.php b/tests/PHPStan/Rules/Classes/data/bug-9946.php new file mode 100644 index 0000000000..e2cb92f5f2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9946.php @@ -0,0 +1,21 @@ +format('c'); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-attributes.php b/tests/PHPStan/Rules/Classes/data/class-attributes.php index bba67367b7..c0cf4238fe 100644 --- a/tests/PHPStan/Rules/Classes/data/class-attributes.php +++ b/tests/PHPStan/Rules/Classes/data/class-attributes.php @@ -122,3 +122,47 @@ class Blebleh { } + +#[\Attribute] +interface InterfaceAsAttribute +{ + +} + +#[InterfaceAsAttribute] +class ClassWithInterfaceAttribute +{} + +#[\Attribute] +trait TraitAsAttribute +{ + +} + +#[TraitAsAttribute] +class ClassWithTraitAttribute +{} + +#[\Attribute(flags: \Attribute::TARGET_CLASS)] +class FlagsAttributeWithClassTarget +{ + +} + +#[\Attribute(flags: \Attribute::TARGET_PROPERTY)] +class FlagsAttributeWithPropertyTarget +{ + +} + +#[FlagsAttributeWithClassTarget] +class TestFlagsAttribute +{ + +} + +#[FlagsAttributeWithPropertyTarget] +class TestWrongFlagsAttribute +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php new file mode 100644 index 0000000000..618fc491b2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php @@ -0,0 +1,61 @@ += 8.2 + +namespace ClassConstantAccessedOnTrait; + +trait Foo +{ + public const TEST = 1; +} + +class Bar +{ + use Foo; +} + +function (): void { + echo Foo::TEST; + echo Foo::class; +}; diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-nullsafe.php b/tests/PHPStan/Rules/Classes/data/class-constant-nullsafe.php new file mode 100644 index 0000000000..4d9ecf91bb --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-constant-nullsafe.php @@ -0,0 +1,17 @@ += 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 index 7e4d7b8677..137b2f58b7 100644 --- a/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php +++ b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php @@ -16,6 +16,7 @@ public function doFoo( echo $string::class; echo $stdOrNull::class; echo $stringOrNull::class; + echo 'Foo'::class; } } diff --git a/tests/PHPStan/Rules/Classes/data/class-extends-enum.php b/tests/PHPStan/Rules/Classes/data/class-extends-enum.php new file mode 100644 index 0000000000..3031b505cd --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-extends-enum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ClassExtendsEnum; + +enum FooEnum +{ + +} + +class Foo extends FooEnum +{ + +} + +function (): void { + new class() extends FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/class-implements-enum.php b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php new file mode 100644 index 0000000000..a6bcab0e7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ClassImplementsEnum; + +enum FooEnum +{ + +} + +class Foo implements FooEnum +{ + +} + +function (): void { + new class() implements FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/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/dynamic-constant-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php new file mode 100644 index 0000000000..09c0b176fe --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php @@ -0,0 +1,47 @@ += 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::{$bar}; + 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/dynamic-constant-stringable-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php new file mode 100644 index 0000000000..e944e19947 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php @@ -0,0 +1,42 @@ += 8.3 + +namespace ClassConstantDynamicStringableAccess; + +use Stringable; +use DateTime; +use DateTimeImmutable; + +abstract class Foo +{ + + public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void + { + echo self::{$mixed}; + echo self::{$nullableStr}; + echo self::{$nullableStringable}; + echo self::{$int}; + echo self::{$nullableInt}; + echo self::{$datetimeOrStr}; + echo self::{1111}; + echo self::{(string)$stringable}; + echo self::{$stringable}; // Uncast Stringable objects will cause a runtime error + } + +} + +final class Bar extends Foo +{ + + public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void + { + echo parent::{$mixed}; + echo self::{$mixed}; + } + + public function testClassDynamic(DateTime|DateTimeImmutable $datetime, object $obj, mixed $mixed): void + { + echo $datetime::{$mixed}; + echo $obj::{$mixed}; + } + +} 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-readonly-class.php b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php new file mode 100644 index 0000000000..cf134c3a2b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php @@ -0,0 +1,41 @@ + 'App\GeneratedProxy\__CG__', + ]; + } + +} + +$doctrineEntity = new \App\GeneratedProxy\__CG__\App\TestDoctrineEntity(); +$phpStanEntity = new \_PHPStan_15755dag8c\TestPhpStanEntity(); diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php new file mode 100644 index 0000000000..9faf5f715a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php @@ -0,0 +1,91 @@ += 8.0 + +namespace ImpossibleInstanceofNewIsAlwaysFinal; + +interface Foo +{ + +} + +class Bar +{ + +} + +function (): void { + $bar = new Bar(); + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if ($bar::class !== Bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (Bar::class !== $bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (get_class($bar) !== Bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (Bar::class !== get_class($bar)) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (): void { + $bar = null; + if (rand(0,1)===1) { + $bar = new Bar(); + } + if ($bar instanceof Foo) { + + } +}; + +class Baz extends Bar +{ + +} + +function (): void { + $bar = null; + if (rand(0,1)===1) { + $bar = new Bar(); + } + if ($bar instanceof Baz) { + + } +}; diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php new file mode 100644 index 0000000000..09c434a4fa --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php @@ -0,0 +1,41 @@ +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 InstantiationNewStaticConsistentConstructor; + +/** + * @phpstan-consistent-constructor + */ +class Foo +{ + + public function __construct(int $i) + { + + } + + public function doFoo(): void + { + $a = new static('s'); + } + +} + +class BaseClass { + public function __construct(protected string $value) { + } +} + +/** + * @phpstan-consistent-constructor + */ +class ChildClass2 extends BaseClass +{ + +} + +class ChildClass3 extends ChildClass2 { + public function fromInt(int $value): static { + return new static($value); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php index c8e8ed29f0..d113ac2f70 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php @@ -1,4 +1,4 @@ -= 8.0 += 8.1 + +namespace InterfaceExtendsEnum; + +enum FooEnum +{ + +} + +interface Foo extends FooEnum +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/internal-constructor.php b/tests/PHPStan/Rules/Classes/data/internal-constructor.php new file mode 100644 index 0000000000..ba1dfd596e --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/internal-constructor.php @@ -0,0 +1,22 @@ += 8.1 + +namespace LocalTypeAliasesEnums; + +/** + * @phpstan-import-type Test from NonexistentClass + */ +enum Foo +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php index 44ed116a70..152e77d8d7 100644 --- a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php @@ -5,7 +5,7 @@ class ExistingClassAlias {} /** - * @phpstan-type ExportedTypeAlias \Countable&\Traversable + * @phpstan-type ExportedTypeAlias \Countable&\Traversable */ class Foo { @@ -62,3 +62,45 @@ class Generic class Invalid { } + +/** @psalm-type MyObject = what{} */ +class InvalidTypeDefinitionToIgnoreBecauseItsAParseErrorAlreadyReportedInInvalidPhpDocTagValueRule +{ + +} + +/** + * @phpstan-type NoIterableValue = array + * @phpstan-type NoGenerics = Generic + * @phpstan-type NoCallable = array + */ +class MissingTypehints +{ + +} + +/** + * @phpstan-type A = Nonexistent + * @phpstan-type B = \LocalTypeTraitAliases\Foo + * @phpstan-type C = fOO + */ +class NonexistentClasses +{ + +} + +/** + * @phpstan-type A = string&int + */ +class UnresolvableExample +{ + +} + +/** + * @phpstan-type A = \Exception + */ +class GenericsCheck +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php new file mode 100644 index 0000000000..6628e0db7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php @@ -0,0 +1,85 @@ + + */ +trait Foo +{ +} + +/** + * @phpstan-type LocalTypeAlias int + * @phpstan-type ExistingClassAlias \stdClass + * @phpstan-type GlobalTypeAlias bool + * @phpstan-type int \stdClass + * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[] + * @phpstan-type CircularTypeAlias1 CircularTypeAlias2 + * @phpstan-type CircularTypeAlias2 CircularTypeAlias1 + */ +trait Bar +{ +} + +/** + * @phpstan-import-type ImportedAliasFromNonClass from int + * @phpstan-import-type ImportedAliasFromUnknownClass from UnknownClass + * @phpstan-import-type ImportedUnknownAlias from Foo + * @phpstan-import-type ExportedTypeAlias from Foo as ExistingClassAlias + * @phpstan-import-type ExportedTypeAlias from Foo as GlobalTypeAlias + * @phpstan-import-type ExportedTypeAlias from Foo as OverwrittenTypeAlias + * @phpstan-import-type ExportedTypeAlias from Foo as int + * @phpstan-type OverwrittenTypeAlias string + * @phpstan-import-type CircularTypeAliasImport1 from Qux + * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 + */ +trait Baz +{ +} + +/** + * @phpstan-import-type CircularTypeAliasImport2 from Baz + * @phpstan-type CircularTypeAliasImport1 CircularTypeAliasImport2 + */ +trait Qux +{ +} + +/** + * @phpstan-template T + * @phpstan-type T never + */ +trait Generic +{ +} + +/** + * @phpstan-type InvalidTypeAlias invalid-type-definition + */ +trait Invalid +{ +} + +/** + * @phpstan-type NoIterablueValue = array + */ +trait MissingType +{ + +} + +class Usages +{ + + use Foo; + use Bar; + use Baz; + use Qux; + use Generic; + use Invalid; + use MissingType; + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php new file mode 100644 index 0000000000..94e019280c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php @@ -0,0 +1,26 @@ + + */ +trait Foo +{ + +} + +class Usage +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php new file mode 100644 index 0000000000..9855c17844 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php @@ -0,0 +1,18 @@ += 8.1 + +namespace MethodTagTraitEnum; + +/** + * @method intt doFoo() + */ +trait Foo +{ + +} + +enum FooEnum +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php new file mode 100644 index 0000000000..504033696a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php @@ -0,0 +1,30 @@ + doA() + * @method Generic doB() + * @method Generic doC() + * @method Generic doD() + */ +class TestGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingIterableValue +{ + +} + +/** + * @method Generic doA() + */ +class MissingCallableSignature +{ + +} + +/** + * @method Nonexistent doA() + * @method \PropertyTagTrait\Foo doB() + * @method fOO doC() + */ +class NonexistentClasses +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin-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/named-arguments-phpversion.php b/tests/PHPStan/Rules/Classes/data/named-arguments-phpversion.php new file mode 100644 index 0000000000..dbba869398 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/named-arguments-phpversion.php @@ -0,0 +1,57 @@ += 8.0 + +declare(strict_types = 1); + +namespace NamedArgumentsPhpversion; + +use Exception; + +class HelloWorld +{ + /** @return mixed[] */ + public function sayHello(): array|null + { + if(PHP_VERSION_ID >= 80400) { + } else { + } + return [ + new Exception(previous: new Exception()), + ]; + } +} + +class HelloWorld2 +{ + /** @return mixed[] */ + public function sayHello(): array|null + { + return [ + PHP_VERSION_ID >= 80400 ? 1 : 0, + new Exception(previous: new Exception()), + ]; + } +} + +class HelloWorld3 +{ + /** @return mixed[] */ + public function sayHello(): array|null + { + return [ + PHP_VERSION_ID >= 70400 ? 1 : 0, + new Exception(previous: new Exception()), + ]; + } +} + +class HelloWorld4 +{ + /** @return mixed[] */ + public function sayHello(): array|null + { + return [ + PHP_VERSION_ID < 80000 ? 1 : 0, + new Exception(previous: new Exception()), + ]; + } +} 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/sealed.php b/tests/PHPStan/Rules/Classes/data/sealed.php new file mode 100644 index 0000000000..d512db7f68 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/sealed.php @@ -0,0 +1,31 @@ += 8.1 + +namespace TraitUseEnum; + +enum FooEnum +{ + +} + +class Foo +{ + + use FooEnum; + +} + +function (): void { + new class() { + + use FooEnum; + + }; +}; diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index e18f5a4486..aa55dbe441 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -2,28 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + /** - * @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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -54,7 +61,7 @@ public function testRule(): void 27, ], [ - 'Right side of && is always false.', + 'Result of && is always false.', 30, ], [ @@ -112,6 +119,99 @@ public function testRule(): void 'Right side of && is always true.', 147, ], + [ + 'Left side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalAnd(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-and.php'], [ + [ + 'Left side of and is always true.', + 15, + ], + [ + 'Right side of and is always true.', + 19, + ], + [ + 'Left side of and is always false.', + 24, + ], + [ + 'Right side of and is always false.', + 27, + ], + [ + 'Result of and is always false.', + 30, + ], + [ + 'Right side of and is always true.', + 33, + ], + [ + 'Right side of and is always true.', + 36, + ], + [ + 'Right side of and is always true.', + 39, + ], + [ + 'Result of and is always false.', + 50, + ], + [ + 'Result of and is always true.', + 54, + $tipText, + ], + [ + 'Result of and is always false.', + 60, + ], + [ + 'Result of and is always true.', + 64, + //$tipText, + ], + [ + 'Result of and is always false.', + 66, + //$tipText, + ], + [ + 'Result of and is always false.', + 125, + ], + [ + 'Left side of and is always false.', + 139, + ], + [ + 'Right side of and is always false.', + 141, + ], + [ + 'Left side of and is always true.', + 145, + ], + [ + 'Right side of and is always true.', + 147, + ], ]); } @@ -161,7 +261,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -173,10 +273,7 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain - */ + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -192,7 +289,7 @@ public function testBugComposerDependentVariables(): void public function testBug2231(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-2231.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-2231.php'], [ [ 'Result of && is always false.', 21, @@ -200,4 +297,152 @@ public function testBug2231(): void ]); } + public function testBug1746(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-1746.php'], [ + [ + 'Left side of && is always true.', + 20, + ], + ]); + } + + public function testBug4666(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4666.php'], []); + } + + public function testBug2870(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2870.php'], []); + } + + 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 static 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%.', + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataBug4969')] + public function testBug4969(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-4969.php'], $expectedErrors); + } + + public static 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%.', + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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 testBug11908(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-11908.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 bfa0081cd6..24df1d9ea1 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -2,28 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + /** - * @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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -60,6 +67,11 @@ public function testRule(): void 'Negated boolean expression is always false.', 50, ], + [ + 'Negated boolean expression is always true.', + 67, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -90,7 +102,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -102,14 +114,89 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain - */ + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; $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 static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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 bf0cffe434..a1a399005e 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -2,28 +2,36 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -58,7 +66,7 @@ public function testRule(): void 30, ], [ - 'Right side of || is always false.', + 'Result of || is always true.', 33, ], [ @@ -103,6 +111,90 @@ public function testRule(): void 'Right side of || is always true.', 85, ], + [ + 'Left side of || is always true.', + 101, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of || is always true.', + 110, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalOr(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-or.php'], [ + [ + 'Left side of or is always true.', + 15, + ], + [ + 'Right side of or is always true.', + 19, + ], + [ + 'Left side of or is always false.', + 24, + ], + [ + 'Right side of or is always false.', + 27, + ], + [ + 'Right side of or is always true.', + 30, + ], + [ + 'Result of or is always true.', + 33, + ], + [ + 'Right side of or is always false.', + 36, + ], + [ + 'Right side of or is always false.', + 39, + ], + [ + 'Result of or is always true.', + 50, + $tipText, + ], + [ + 'Result of or is always true.', + 54, + $tipText, + ], + [ + 'Result of or is always true.', + 61, + ], + [ + 'Result of or is always true.', + 65, + ], + [ + 'Left side of or is always false.', + 77, + ], + [ + 'Right side of or is always false.', + 79, + ], + [ + 'Left side of or is always true.', + 83, + ], + [ + 'Right side of or is always true.', + 85, + ], ]); } @@ -152,7 +244,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -164,14 +256,119 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain - */ + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; $this->analyse([__DIR__ . '/data/boolean-or-treat-phpdoc-types-regression.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug6258(): void + { + $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 static 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%.', + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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..e1a9a3db03 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -0,0 +1,250 @@ + + */ +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%.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8485(): void + { + $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 static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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 static 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%.', + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataTreatPhpDocTypesAsCertain')] + 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, + ], + ]); + } + + public function testBug13098(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13098.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index c064156ea5..f83f2cf1b0 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -2,36 +2,32 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DoWhileLoopConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class DoWhileLoopConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain = true; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DoWhileLoopConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), + true, ); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - public function testRule(): void { $this->analyse([__DIR__ . '/data/do-while-loop.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 31cb731e65..9eec3deec1 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -2,28 +2,36 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -32,16 +40,57 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - - public function testRule(): void + public static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataRule')] + 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 +99,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 +111,41 @@ 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.', + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11674(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11674.php'], [ + [ + 'Elseif condition is always false.', + 28, + ], + [ + 'Elseif condition is always false.', + 36, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6947(): void + { + $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 4623df6293..2bf3017098 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -2,28 +2,32 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + true, ); } @@ -48,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.', @@ -114,4 +119,79 @@ public function testBug4043(): void ]); } + public function testBug5370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5370.php'], []); + } + + public function testBug6902(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8485(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13384b(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../TooWideTypehints/data/bug-13384b.php'], [ + [ + 'If condition is always false.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 8e7633a5df..c795706ab9 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -2,29 +2,38 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use stdClass; +use function array_filter; +use function array_map; +use function array_values; +use function count; + /** - * @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(), + self::createReflectionProvider(), $this->getTypeSpecifier(), - [\stdClass::class], - $this->treatPhpDocTypesAsCertain + [stdClass::class], + $this->treatPhpDocTypesAsCertain, ), - $this->checkAlwaysTrueCheckTypeFunctionCall, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -33,9 +42,9 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + #[RequiresPhp('>= 8.0')] public function testImpossibleCheckTypeFunctionCall(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse( [__DIR__ . '/data/check-type-function-call.php'], @@ -81,270 +90,203 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function is_string() with string will always evaluate to true.', 140, ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'test\' will always evaluate to false.', + 176, + ], [ 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'doFoo\' will always evaluate to true.', 179, ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'doFoo\' will always evaluate to true.', + 189, + ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doFoo\' will always evaluate to true.', - 191, + 201, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doBar\' will always evaluate to false.', - 194, + 204, ], [ 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'fooProperty\' will always evaluate to true.', - 209, - ], - [ - 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'barProperty\' will always evaluate to false.', - 212, + 220, ], [ - 'Call to function in_array() with arguments int, array(\'foo\', \'bar\') and true will always evaluate to false.', - 235, + '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(\'baz\', \'lorem\') and true will always evaluate to false.', - 244, + '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 \'bar\'|\'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', - 248, + 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', + 263, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\') and true will always evaluate to true.', - 252, + 'Call to function in_array() with arguments \'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', + 267, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', - 256, + 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', + 331, ], [ - '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.', + 347, ], [ - '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.', + 354, ], [ - 'Call to function in_array() with arguments \'foo\', array() and true will always evaluate to false.', - 343, + 'Call to function array_key_exists() with \'a\' and array{a: 1, b?: 2} will always evaluate to true.', + 371, ], [ - 'Call to function array_key_exists() with \'a\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to true.', - 360, - ], - [ - 'Call to function array_key_exists() with \'c\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to false.', - 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.', + 620, + ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'undefinedMethod\' will always evaluate to false.', + 623, ], [ 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'testWithNewObjectIn…\' will always evaluate to true.', - 609, + 635, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.', - 624, + 650, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'someAnother\' will always evaluate to true.', - 627, + 653, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 630, + 656, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', - 633, + 659, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', - 636, + 662, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 639, + 665, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', - 642, + 668, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', - 645, + 671, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 648, + 674, ], [ 'Call to function is_string() with string will always evaluate to true.', - 677, + 703, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function assert() with true will always evaluate to true.', - 692, + 718, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'123\' will always evaluate to true.', - 692, + 718, ], [ 'Call to function assert() with false will always evaluate to false.', - 693, + 719, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 693, + 719, ], [ 'Call to function assert() with true will always evaluate to true.', - 700, + 726, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with 123|float will always evaluate to true.', - 700, + 726, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 782, + 809, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 786, - ], - ] - ); - } - - public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueCheckTypeFunctionCall = false; - $this->treatPhpDocTypesAsCertain = true; - $this->analyse( - [__DIR__ . '/data/check-type-function-call.php'], - [ - [ - 'Call to function is_int() with string will always evaluate to false.', - 31, - ], - [ - 'Call to function is_callable() with array will always evaluate to false.', - 44, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', - ], - [ - 'Call to function assert() with false will always evaluate to false.', - 48, - ], - [ - 'Call to function is_callable() with \'nonexistentFunction\' will always evaluate to false.', - 87, - ], - [ - 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 105, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doBar\' will always evaluate to false.', - 194, - ], - [ - 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'barProperty\' will always evaluate to false.', - 212, - ], - [ - 'Call to function in_array() with arguments int, array(\'foo\', \'bar\') and true will always evaluate to false.', - 235, - ], - [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array(\'baz\', \'lorem\') and true will always evaluate to false.', - 244, - ], - [ - 'Call to function in_array() with arguments \'bar\', array()|array(\'foo\') and true will always evaluate to false.', - 320, - ], - [ - 'Call to function in_array() with arguments \'baz\', array(0 => \'bar\', ?1 => \'foo\') and true will always evaluate to false.', - 336, - ], - [ - 'Call to function in_array() with arguments \'foo\', array() and true will always evaluate to false.', - 343, - ], - [ - 'Call to function array_key_exists() with \'c\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to false.', - 366, - ], - [ - 'Call to function is_string() with mixed will always evaluate to false.', - 560, - ], - [ - 'Call to function is_callable() with mixed will always evaluate to false.', - 571, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 594, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 597, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 630, - ], - [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 639, + 813, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 648, + 'Call to function testIsInt() with int will always evaluate to true.', + 900, ], [ - 'Call to function assert() with false will always evaluate to false.', - 693, + 'Call to function is_int() with int will always evaluate to true.', + 914, + 'Remove remaining cases below this one and this error will disappear too.', ], [ - 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 693, + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 952, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], - ] + ], ); } + public function testBug7898(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7898.php'], []); + } + public function testDoNotReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ @@ -356,7 +298,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'], [ [ @@ -368,49 +309,822 @@ public function testReportTypesFromPhpDocs(): void 19, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + 'Call to function in_array() with arguments int, array and true will always evaluate to false.', + 27, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } public function testBug2550(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2550.php'], []); } public function testBug3994(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3994.php'], []); } public function testBug1613(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-1613.php'], []); } public function testBug2714(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2714.php'], []); } public function testBug4657(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-4657.php'], []); } public function testBug4999(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-4999.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testArrayIsList(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testConditionalTypesInference(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6443(): void + { + $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 static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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, + ], + ]; + } + + #[RequiresPhp('>= 8.1')] + public function testLooseComparisonAgainstEnums(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testNonStrictInArrayEnums(): 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__ . '/../../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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8464(): void + { + $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, + ], + ]); + } + + public function testBug13151(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13151.php'], []); + } + + public function testBug8818(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8818.php'], []); + } + + public function testBug12755(): 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-12755.php'], [ + [ + 'Call to function in_array() with arguments null, array{key1: bool|null, key2: null} and true will always evaluate to true.', + 51, + $tipText, + ], + ]); + } + + public function testBugStrictRule147(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-strict-147.php'], []); + } + + public function testBugStrictRule143(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-strict-143.php'], []); + } + + public function testBug12412(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12412.php'], []); + } + + public function testBug2730(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2730.php'], [ + [ + 'Call to function is_object() with int will always evaluate to false.', + 43, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testBug13291(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13291.php'], []); + } + + public function testBug6788(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6788.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13268(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13268.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12087(): 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-12087.php'], [ + [ + 'Call to function is_null() with null will always evaluate to true.', + 14, + $tipText, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12087c(): 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-12087c.php'], [ + [ + 'Call to function is_null() with null will always evaluate to true.', + 17, + $tipText, + ], + [ + 'Call to function is_null() with 10 will always evaluate to false.', + 23, + ], + [ + 'Call to function is_null() with null will always evaluate to true.', + 29, + ], + ]); + } + + public function testBug9666(): 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-9666.php'], [ + [ + 'Call to function is_bool() with bool will always evaluate to true.', + 20, + $tipText, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9445(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9445.php'], []); + } + + public function testBug7773(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7773.php'], []); + } + + public function testPr4375(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pr-4375.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php new file mode 100644 index 0000000000..aff6f8d5df --- /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( + self::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..2eec07890a --- /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( + self::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 bd5c278609..fb64511aff 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -2,26 +2,33 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - public function getRule(): \PHPStan\Rules\Rule + public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, true, - $this->treatPhpDocTypesAsCertain ); } @@ -45,10 +52,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.', @@ -70,6 +79,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.', + ], ]); } @@ -108,6 +200,96 @@ public function testReportPhpDoc(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug8169(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8169.php'], [ + [ + 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', + 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 static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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, + ],*/ + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12087b(): 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-12087b.php'], [ + [ + 'Call to method Bug12087b\MyAssert::is_null() with null will always evaluate to true.', + 37, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index fcaf5e3fea..8a68085379 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -2,26 +2,33 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeStaticMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - public function getRule(): \PHPStan\Rules\Rule + public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, true, - $this->treatPhpDocTypesAsCertain ); } @@ -58,6 +65,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.', + ], ]); } @@ -96,6 +108,59 @@ public function testReportPhpDocs(): void ]); } + public function testAssertUnresolvedGeneric(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/assert-unresolved-generic.php'], []); + } + + public static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + 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); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12087b(): 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-12087b.php'], [ + [ + 'Call to static method Bug12087b\MyAssert::static_is_null() with null will always evaluate to true.', + 31, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php new file mode 100644 index 0000000000..7f9146b058 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -0,0 +1,73 @@ + + */ +class LogicalXorConstantConditionRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new LogicalXorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + $this->shouldTreatPhpDocTypesAsCertain(), + false, + 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->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..f81136f8d6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php @@ -0,0 +1,43 @@ + + */ +class MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(MatchExpressionRule::class); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9357(): void + { + $this->analyse([__DIR__ . '/data/bug-9357.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9007(): void + { + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/doNotRememberPossiblyImpureValues.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 39f6610643..014b57899c 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -11,17 +12,32 @@ class MatchExpressionRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new MatchExpressionRule(true); + return new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ); } - public function testRule(): void + protected function shouldTreatPhpDocTypesAsCertain(): bool { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + return $this->treatPhpDocTypesAsCertain; + } + public function testRule(): void + { + $tipText = 'Remove remaining cases below this one and this error will disappear too.'; $this->analyse([__DIR__ . '/data/match-expr.php'], [ [ 'Match arm comparison between 1|2|3 and \'foo\' is always false.', @@ -34,83 +50,53 @@ public function testRule(): void [ 'Match arm comparison between 3 and 3 is always true.', 28, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 29, + $tipText, ], [ 'Match arm comparison between 3 and 3 is always true.', 35, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 36, + $tipText, ], [ 'Match arm comparison between 1 and 1 is always true.', 40, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 41, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 42, + $tipText, ], [ 'Match arm comparison between 1 and 1 is always true.', 46, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 47, + $tipText, ], [ 'Match expression does not handle remaining value: 3', 50, ], - [ - 'Match expression does not handle remaining values: 1|2|3', - 55, - ], [ 'Match arm comparison between 1|2 and 3 is always false.', - 65, + 61, ], [ - 'Match arm comparison between 1 and 1 is always true.', - 70, - ], - [ - 'Match arm comparison between true and false is always false.', - 86, - ], - [ - 'Match arm comparison between true and false is always false.', - 92, + 'Match expression does not handle remaining values: 1|2|3', + 78, ], [ 'Match expression does not handle remaining value: true', 90, ], + [ + 'Match expression does not handle remaining values: int|int<2, max>', + 168, + ], ]); } public function testBug5161(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5161.php'], []); } public function testBug4857(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-4857.php'], [ [ 'Match expression does not handle remaining value: true', @@ -123,12 +109,304 @@ public function testBug4857(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug5454(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5454.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6394(): void + { + $this->analyse([__DIR__ . '/data/bug-6394.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6115(): void + { + $this->analyse([__DIR__ . '/data/bug-6115.php'], [ + [ + 'Match expression does not handle remaining value: 3', + 32, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7095(): void + { + $this->analyse([__DIR__ . '/data/bug-7095.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7176(): void + { + $this->analyse([__DIR__ . '/data/bug-7176.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6064(): void + { + $this->analyse([__DIR__ . '/data/bug-6064.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6647(): void + { + $this->analyse([__DIR__ . '/data/bug-6647.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7622(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7622.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7698(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7698.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7746(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7746.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8240(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testLastArmAlwaysTrue(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testLastCondition(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/match-always-true-last-arm.php'], [ + [ + '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.', + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8932(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8932.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8937(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8937.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8900(): void + { + $this->analyse([__DIR__ . '/data/bug-8900.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug4451(): void + { + $this->analyse([__DIR__ . '/data/bug-4451.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9007(): void + { + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9457(): void + { + $this->analyse([__DIR__ . '/data/bug-9457.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8614(): void + { + $this->analyse([__DIR__ . '/data/bug-8614.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8536(): void + { + $this->analyse([__DIR__ . '/data/bug-8536.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9499(): void + { + $this->analyse([__DIR__ . '/data/bug-9499.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6407(): void + { + $this->analyse([__DIR__ . '/data/bug-6407.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBugUnhandledTrueWithComplexCondition(): void + { + $this->analyse([__DIR__ . '/data/bug-unhandled-true-with-complex-condition.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11246(): void + { + $this->analyse([__DIR__ . '/data/bug-11246.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9879(): void + { + $this->analyse([__DIR__ . '/data/bug-9879.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11313(): void + { + $this->analyse([__DIR__ . '/data/bug-11313.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9436(): void + { + $this->analyse([__DIR__ . '/data/bug-9436.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11852(): void + { + $this->analyse([__DIR__ . '/data/bug-11852.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $this->analyse([__DIR__ . '/data/match-expr-property-hooks.php'], [ + [ + 'Match expression does not handle remaining value: 3', + 13, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13048(): void + { + $this->analyse([__DIR__ . '/data/bug-13048.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 7d2b12664a..a6981ba1b9 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -4,16 +4,28 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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 @@ -46,10 +58,209 @@ public function testBug2648Namespace(): void public function testBug5161(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5161.php'], []); } + 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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7052(): void + { + $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 static 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%.', + ], + ], + ]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataTreatPhpDocTypesAsCertain')] + 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 testBug9180(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9180.php'], []); + } + + public function testBug12716(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12716.php'], []); + } + + public function testBug3387(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3387.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 6ab3efd944..75529afc48 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,23 +2,42 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Analyser\RicherScopeGetTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +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(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison); + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; } public function testStrictComparison(): void { - $this->checkAlwaysTrueStrictComparison = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/strict-comparison.php'], [ @@ -45,6 +64,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 +103,26 @@ public function testStrictComparison(): void 130, ], [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 140, ], [ - '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&nonEmpty 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 +133,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,33 +181,26 @@ public function testStrictComparison(): void 426, ], [ - 'Strict comparison using === between int and null will always evaluate to false.', // todo remove with isDeterministic - 438, - ], - [ - 'Strict comparison using === between int|int<2, max>|string and 1.0 will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and 1.0 will always evaluate to false.', 464, ], [ - 'Strict comparison using === between int|int<2, max>|string and stdClass will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and stdClass will always evaluate to false.', 466, ], [ 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', 622, + $tipText, ], [ 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', 624, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', + 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', 635, ], - [ - 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', - 654, - ], [ 'Strict comparison using === between string|null and 1 will always evaluate to false.', 685, @@ -197,10 +216,12 @@ public function testStrictComparison(): void [ 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', 808, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using !== between mixed and 1 will always evaluate to true.', 812, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using === between \'foo\' and \'foo\' will always evaluate to true.', @@ -230,155 +251,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 1 and 1.0 will always evaluate to false.', - 47, - ], - [ - 'Strict comparison using === between string and null will always evaluate to false.', - 69, - ], - [ - 'Strict comparison using === between 1|2|3 and null will always evaluate to false.', - 98, - ], - [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', - 140, - ], - [ - 'Strict comparison using === between array&nonEmpty 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 INF and INF will always evaluate to true.', + 979, ], [ - 'Strict comparison using === between int|int<2, max>|string and 1.0 will always evaluate to false.', - 464, + 'Strict comparison using === between NAN and NAN will always evaluate to false.', + 980, ], [ - 'Strict comparison using === between int|int<2, max>|string and stdClass will always evaluate to false.', - 466, + 'Strict comparison using !== between INF and INF will always evaluate to false.', + 982, ], [ - 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', - 622, + 'Strict comparison using !== between NAN and NAN will always evaluate to true.', + 983, ], [ - 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', - 624, - ], - [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', - 635, + 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', + 996, + 'Remove remaining cases below this one and this error will disappear too.', ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 685, + 'Strict comparison using === between lowercase-string|false and \'AB\' will always evaluate to false.', + 1014, + $tipText, ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 695, + 'Strict comparison using === between mixed and null will always evaluate to false.', + 1030, + 'Type null has already been eliminated from mixed.', ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 705, + 'Strict comparison using !== between mixed and null will always evaluate to true.', + 1034, + 'Type null has already been eliminated from mixed.', ], [ - 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', - 808, + 'Strict comparison using !== between array{1, mixed, 3} and array{int, null, int} will always evaluate to true.', + 1048, + 'Offset 1: Type null has already been eliminated from mixed.', ], - ] + ], ); } 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.', @@ -389,10 +308,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.', @@ -415,13 +330,11 @@ public function testStrictComparisonPropertyNativeTypesPhp74(): void public function testBug2835(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2835.php'], []); } public function testBug1860(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-1860.php'], [ [ 'Strict comparison using === between string and null will always evaluate to false.', @@ -436,37 +349,34 @@ public function testBug1860(): void public function testBug3544(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3544.php'], []); } public function testBug2675(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2675.php'], []); } public function testBug2220(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2220.php'], []); } public function testBug1707(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-1707.php'], []); } public function testBug3357(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3357.php'], []); } public function testBug4848(): void { - $this->checkAlwaysTrueStrictComparison = true; + 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.', @@ -477,20 +387,657 @@ public function testBug4848(): void public function testBug4793(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-4793.php'], []); } public function testBug5062(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-5062.php'], []); } public function testBug3366(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3366.php'], []); } + public function testBug5362(): void + { + $this->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 testBug4993(): void + { + $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ + 'Strict comparison using === between non-empty-list and null will always evaluate to false.', + 11, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-4993.php'], $errors); + } + + 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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8485(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug4242(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6260(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6260.php'], []); + } + + public function testBug8736(): void + { + $this->analyse([__DIR__ . '/data/bug-8736.php'], []); + } + + public static 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, + ], + ]]; + } + + /** + * @param list $expectedErrors + */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataLastMatchArm')] + public function testLastMatchArm(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $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%.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnumTips(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9142(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug4061(): void + { + $this->analyse([__DIR__ . '/data/bug-4061.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9723(): void + { + $this->analyse([__DIR__ . '/data/bug-9723.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9723b(): void + { + $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 testBug3803(): void + { + $this->analyse([__DIR__ . '/data/bug-3803.php'], []); + } + + public function testBug11019(): void + { + $this->analyse([__DIR__ . '/data/bug-11019.php'], []); + } + + public function testBug12946(): void + { + $this->analyse([__DIR__ . '/data/bug-12946.php'], []); + } + + public function testBug10884(): void + { + $this->analyse([__DIR__ . '/data/bug-10884.php'], []); + } + + public function testBug3761(): void + { + $this->analyse([__DIR__ . '/data/bug-3761.php'], []); + } + + public function testBug13208(): void + { + $this->analyse([__DIR__ . '/data/bug-13208.php'], []); + } + + public function testBug11609(): void + { + $this->analyse([__DIR__ . '/data/bug-11609.php'], [ + [ + 'Strict comparison using !== between string and null will always evaluate to true.', + 10, + '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/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index d68bf08601..250f639068 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -2,28 +2,31 @@ 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( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + true, ); } @@ -90,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 index bf873a965c..f0ec810999 100644 --- a/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php @@ -18,10 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/void-match.php'], [ [ 'Result of match expression (void) is used.', diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index 7914aa6a03..c434368b47 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -2,36 +2,32 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class WhileLoopAlwaysFalseConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class WhileLoopAlwaysFalseConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain = true; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new WhileLoopAlwaysFalseConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), + true, ); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - public function testRule(): void { $this->analyse([__DIR__ . '/data/while-loop-false.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index e077e19f48..45bcf32a83 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -2,36 +2,32 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class WhileLoopAlwaysTrueConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class WhileLoopAlwaysTrueConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain = true; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new WhileLoopAlwaysTrueConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), ), - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), + true, ); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - public function testRule(): void { $this->analyse([__DIR__ . '/data/while-loop-true.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php index 7d8e0f298f..258f5251a9 100644 --- a/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php +++ b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php @@ -99,7 +99,7 @@ public function specifyTypes( $node->args[0]->value, $node->args[1]->value ), - TypeSpecifierContext::createTruthy() + $context ); } @@ -144,7 +144,97 @@ public function specifyTypes( $node->args[0]->value, $node->args[1]->value ), - TypeSpecifierContext::createTruthy() + $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 @@ +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 @@ + $map */ +$map = new \SplObjectStorage(); +$map->attach(new Cat()); +$map->attach(new Cat()); + +class Manager +{ + /** + * @param SplObjectStorage $map + */ + public function doSomething(\SplObjectStorage $map): void + { + /** @var \SplObjectStorage $other */ + $other = new \SplObjectStorage(); + + if (count($map) === 0) { + return; + } + + foreach ($map as $cat) { + if (!$this->someCheck($cat)) { + continue; + } + + $other->attach($cat); + } + + $map->removeAll($other); + + if (count($map) === 0) { + return; + } + + // ok! + } + + private function someCheck(Cat $cat): bool { + // just some random + return $cat == true; + } +} + +(new Manager())->doSomething($map); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11019.php b/tests/PHPStan/Rules/Comparison/data/bug-11019.php new file mode 100644 index 0000000000..c6a64cec05 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11019.php @@ -0,0 +1,21 @@ +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-11609.php b/tests/PHPStan/Rules/Comparison/data/bug-11609.php new file mode 100644 index 0000000000..82686787e0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11609.php @@ -0,0 +1,17 @@ +hello !== null) { + echo 'hello'; + } + if ($a->world !== null) { + + } + } +} 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-11908.php b/tests/PHPStan/Rules/Comparison/data/bug-11908.php new file mode 100644 index 0000000000..b583aaef55 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11908.php @@ -0,0 +1,8 @@ += 8.1 + +namespace Bug12087; + +enum Button: int +{ + case On = 1; + + case Off = 0; +} + +$value = 10; + +is_null($value = Button::tryFrom($value)); + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12087b.php b/tests/PHPStan/Rules/Comparison/data/bug-12087b.php new file mode 100644 index 0000000000..3c26c567e9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12087b.php @@ -0,0 +1,38 @@ += 8.1 + +namespace Bug12087b; + +enum Button: int +{ + case On = 1; + + case Off = 0; +} + +class MyAssert { + /** + * @return ($value is null ? true : false) + */ + static public function static_is_null(mixed $value): bool { + return $value === null; + } + + /** + * @return ($value is null ? true : false) + */ + public function is_null(mixed $value): bool { + return $value === null; + } +} + +function doFoo(): void { + $value = 10; + + MyAssert::static_is_null($value = Button::tryFrom($value)); +} + +function doBar(MyAssert $assert): void { + $value = 10; + + $assert->is_null($value = Button::tryFrom($value)); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12087c.php b/tests/PHPStan/Rules/Comparison/data/bug-12087c.php new file mode 100644 index 0000000000..b19be4b824 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12087c.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug12087c; + +enum Button: int +{ + case On = 1; + + case Off = 0; +} + +function doFoo() +{ + $foo = 'abc'; + $value = 10; + + is_null($value = $foo = Button::tryFrom($value)); +} + +function doFoo2() { + $value = 10; + + is_null($value ??= Button::tryFrom($value)); +} + +function doFoo3() { + $value = null; + + is_null($value ??= Button::tryFrom($value)); +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12412.php b/tests/PHPStan/Rules/Comparison/data/bug-12412.php new file mode 100644 index 0000000000..397088dfa8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12412.php @@ -0,0 +1,26 @@ + + */ +function f(string $type): array { + $field_list = [ ]; + if ($type === 'A') { + array_push($field_list, 'x1'); + } + if ($type === 'B') { + array_push($field_list, 'x2'); + } + + assertType('bool', in_array('x1', $field_list, true)); + + array_push($field_list, 'x3'); + assertType('bool', in_array('x1', $field_list, true)); + + return $field_list; +} 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-12755.php b/tests/PHPStan/Rules/Comparison/data/bug-12755.php new file mode 100644 index 0000000000..99bd5faac5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12755.php @@ -0,0 +1,90 @@ + $stack + */ + public function testEnum(array $stack): bool + { + return count($stack) === 1 && in_array(MyEnum::ONE, $stack, true); + } + + /** + * @param array{1|2|3} $stack + * @param array{1|2|3, 1|2|3} $stack2 + * @param array{1|2|3, 2|3} $stack3 + * @param array{a?: 1, b: 2|3} $stack4 + * @param array{a?: 1} $stack5 + */ + public function sayHello(array $stack, array $stack2, array $stack3, array $stack4, array $stack5): void + { + if (in_array(1, $stack, true)) { + } + + if (in_array(1, $stack2, true)) { + } + + if (in_array(1, $stack3, true)) { + } + + if (in_array(1, $stack4, true)) { + } + + if (in_array(1, $stack5, true)) { + } + } +} 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-13048.php b/tests/PHPStan/Rules/Comparison/data/bug-13048.php new file mode 100644 index 0000000000..c2b2248123 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13048.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug13048; + +enum IndexBy { + case A; + case B; +} + +/** + * @template T of IndexBy|null + * @param T $indexBy + */ +function run(?IndexBy $indexBy = null): ?string +{ + return match ($indexBy) { + IndexBy::A => 'by A', + IndexBy::B => 'by B', + null => null, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13098.php b/tests/PHPStan/Rules/Comparison/data/bug-13098.php new file mode 100644 index 0000000000..b549d338fe --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13098.php @@ -0,0 +1,15 @@ + 1, + 'b' => 1, + 'c' => 1 + ]; + } + + public function test(): void + { + echo in_array(0, $this->extractAsArray(), true) ? "True" : "False"; + } + + public function test2(): void + { + echo in_array(0, $this->extractAsArray()) ? "True" : "False"; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13208.php b/tests/PHPStan/Rules/Comparison/data/bug-13208.php new file mode 100644 index 0000000000..c46cc7619f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13208.php @@ -0,0 +1,14 @@ +fwrite('a') === false) { + throw new \Exception("write failed !"); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13268.php b/tests/PHPStan/Rules/Comparison/data/bug-13268.php new file mode 100644 index 0000000000..0bdbd130eb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13268.php @@ -0,0 +1,28 @@ += 8.2 + +namespace Bug13291; + +function test(bool $someBool): true { + var_dump($someBool); + return true; +} + +test(someBool: true); 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-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 @@ + $v){ + if(is_string($v)){ + echo "string\n"; + }elseif(is_object($v)){ + echo "object\n"; + }else{ + echo gettype($v) . "\n"; + } + } + } +} + +class B extends A{ + /** @var \stdClass */ + public $obj; + + public function __construct(){ + $this->obj = new \stdClass; + } +} + +final class C{ + /** @var string */ + public $a = "hi"; + /** @var int */ + public $b = 0; + + public function dummy() : void{ + foreach((array) $this as $k => $v){ + if(is_string($v)){ + echo "string\n"; + }elseif(is_object($v)){ + echo "object\n"; + }else{ + echo gettype($v) . "\n"; + } + } + } +} 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 @@ + '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-3387.php b/tests/PHPStan/Rules/Comparison/data/bug-3387.php new file mode 100644 index 0000000000..ef10797eb8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3387.php @@ -0,0 +1,33 @@ + []]; + foreach ($items as $item) { + $array[$key][] = $item; + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; + +function (array $items, string $key) { + $array = [$key => []]; + foreach ($items as $item) { + array_unshift($array[$key], $item); + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; + +function (array $items, string $key) { + $array = [$key => []]; + foreach ($items as $item) { + array_push($array[$key], $item); + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; 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-3761.php b/tests/PHPStan/Rules/Comparison/data/bug-3761.php new file mode 100644 index 0000000000..01531dbc1c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3761.php @@ -0,0 +1,34 @@ + $class + */ + public function getTagValue(string $class = NamedTag::class) : int{ + $tag = $this->getTag(); + if($tag instanceof $class){ + return $tag->getValue(); + } + + throw new \RuntimeException(($tag === null ? "Missing" : get_class($tag))); + } +} + +(new HelloWorld())->getTagValue(OtherSubclassOfNamedTag::class); //runtime exception: SubclassOfNamedTag 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-3803.php b/tests/PHPStan/Rules/Comparison/data/bug-3803.php new file mode 100644 index 0000000000..0a4db43f68 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3803.php @@ -0,0 +1,16 @@ + $chars */ +function fun(array $chars) : void{ + $string = ""; + foreach($chars as $k => $v){ + $string[$k] = $v; + } + if($string === "wheee"){ + var_dump("yes"); + } +} + +fun(["w", "h", "e", "e", "e"]); 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 @@ += 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-4666.php b/tests/PHPStan/Rules/Comparison/data/bug-4666.php new file mode 100644 index 0000000000..b9e67f0a50 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4666.php @@ -0,0 +1,29 @@ + $objects */ + public function test(array $objects): void + { + $types = []; + foreach ($objects as $object) { + if (self::CONST_1 === $object->getType() && !in_array(self::CONST_2, $types, true)) { + $types[] = self::CONST_2; + } + } + } +} + +class MyObject +{ + /** @var string */ + private $type; + public function getType(): string{ + return $this->type; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4708.php b/tests/PHPStan/Rules/Comparison/data/bug-4708.php new file mode 100644 index 0000000000..2afa164340 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4708.php @@ -0,0 +1,94 @@ + FALSE, + 'dberror' => 'xyz']; + } + else + { + assertType('array|true', $result); + if (!isset($result['bsw'])) + { + assertType('array|true', $result); + $result['bsw'] = 1; + assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result); + } + else + { + assertType('non-empty-array&hasOffsetValue(\'bsw\', string)', $result); + $result['bsw'] = (int) $result['bsw']; + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + + if (!isset($result['bew'])) + { + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + $result['bew'] = 5; + assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bew', int|string)&hasOffsetValue('bsw', int)", $result); + $result['bew'] = (int) $result['bew']; + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + + foreach (['utc', 'ssi'] as $field) + { + if (array_key_exists($field, $result)) + { + $result[$field] = (int) $result[$field]; + } + } + } + + assertType("non-empty-array", $result); + + return $result; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4864.php b/tests/PHPStan/Rules/Comparison/data/bug-4864.php new file mode 100644 index 0000000000..288e19c21f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4864.php @@ -0,0 +1,25 @@ +isHandled = false; + $this->value = null; + + (function () { + $this->isHandled = true; + $this->value = 'value'; + })(); + + if ($this->isHandled) { + $f($this->value); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4912.php b/tests/PHPStan/Rules/Comparison/data/bug-4912.php new file mode 100644 index 0000000000..cdbb585f9a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4912.php @@ -0,0 +1,27 @@ + $requiredRatio) { + + } elseif ($srcRatio < $requiredRatio) { + + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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 @@ + 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-7773.php b/tests/PHPStan/Rules/Comparison/data/bug-7773.php new file mode 100644 index 0000000000..f7619b92a1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7773.php @@ -0,0 +1,40 @@ + $data json array + * @return string json string + * @throws JSONEncodingException + */ + public static function JSONEncode(array $data): string + { + if (!is_string($data = json_encode($data))) + throw new JSONEncodingException(); + return $data; + } + + /** + * Decodes the JSON data as an array + * @param string $data json string + * @return array json array + * @throws JSONDecodingException + */ + public static function JSONDecode(string $data): array + { + if (!is_array($data = json_decode($data, true))) + throw new JSONDecodingException(); + return $data; + } +} 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 @@ + $stack + */ + public function sayHello(array $stack): bool + { + return count($stack) === 1 && in_array(MyEnum::ONE, $stack, true); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8900.php b/tests/PHPStan/Rules/Comparison/data/bug-8900.php new file mode 100644 index 0000000000..1dd0e78839 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8900.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug8900; + +class Foo +{ + + public function doFoo(): void + { + $test_array = []; + for($index = 0; $index++; $index < random_int(1,100)) { + $test_array[] = 'entry'; + } + + foreach($test_array as $key => $value) { + $key_mod_4 = match($key % 4) { + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + }; + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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-9180.php b/tests/PHPStan/Rules/Comparison/data/bug-9180.php new file mode 100644 index 0000000000..d92e3b213c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9180.php @@ -0,0 +1,15 @@ +push(1); + +if ($queue->count() > 0) { + for ($i=0;$i<5;$i++) { + while ($queue->count() > 0 && $value = $queue->shift()) { + //do something with $value + } + } +} 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-9445.php b/tests/PHPStan/Rules/Comparison/data/bug-9445.php new file mode 100644 index 0000000000..8fd828d0cc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9445.php @@ -0,0 +1,22 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug9445; + +class Foo +{ + public int $id; + public null|self $parent; + + public function contains(self $foo): bool + { + do { + if ($this->id === $foo->id) { + return true; + } + } while (!is_null($foo = $foo->parent)); + + return false; + } +} 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-9666.php b/tests/PHPStan/Rules/Comparison/data/bug-9666.php new file mode 100644 index 0000000000..028601f5c2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9666.php @@ -0,0 +1,24 @@ + + */ + function b() + { + return []; + } +} + +function doFoo() { + $a = new A(); + $b = $a->b(); + $c = null; + if ($b && is_bool($c = reset($b))) { + // + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723.php b/tests/PHPStan/Rules/Comparison/data/bug-9723.php new file mode 100644 index 0000000000..eba7197793 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723.php @@ -0,0 +1,39 @@ += 8.1 + +namespace Bug9723; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( $state === State::StateZero->value ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value +// ... or equivalently, $state is 0|1 +// ... but phpstan thinks $state is definitely 0 + if ( $state === State::StateZero->value ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723b.php b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php new file mode 100644 index 0000000000..acbd20b08e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php @@ -0,0 +1,48 @@ += 8.1 + +namespace Bug9723b; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; + case StateTwo = 2; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + case State::StateTwo->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value|State::StateTwo->value +// ... or equivalently, $state is 0|1|2 +// ... but phpstan thinks $state is definitely 0 +// ... and that is is being compared against 0 and 1, not 0 and 2??? + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9804.php b/tests/PHPStan/Rules/Comparison/data/bug-9804.php new file mode 100644 index 0000000000..723f8ba159 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9804.php @@ -0,0 +1,34 @@ +|int<1, max> and 0 will always evaluate to false. + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } + + public function pass(?string $someString): void + { + // Line below is the only difference to "error" method + if ($someString === null) { + return; + } + + // All ok + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9850.php b/tests/PHPStan/Rules/Comparison/data/bug-9850.php new file mode 100644 index 0000000000..2f1ba9e50a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9850.php @@ -0,0 +1,24 @@ += 3) { + // todo + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9879.php b/tests/PHPStan/Rules/Comparison/data/bug-9879.php new file mode 100644 index 0000000000..3223872658 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9879.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug9879; + +final class A { + public function test(): void + { + for($idx = 0; $idx < 6; $idx += 1) { + match($idx % 3) { + 0 => 1, + 1 => 2, + 2 => 0, + }; + } + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php b/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php new file mode 100644 index 0000000000..7dd533ff98 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php @@ -0,0 +1,24 @@ += 8.0 + +namespace BugPR3404; + +interface Location +{ + +} + +/** @return class-string */ +function aaa(): string +{ + +} + +function (Location $l): void { + if (is_a($l, aaa(), true)) { + // might not always be true. $l might be one subtype of Location, aaa() might return a name of a different subtype of Location + } + + if (is_a($l, Location::class, true)) { + // always true + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-strict-143.php b/tests/PHPStan/Rules/Comparison/data/bug-strict-143.php new file mode 100644 index 0000000000..e19396e382 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-strict-143.php @@ -0,0 +1,7 @@ += 8.1 + +namespace MatchUnhandledTrueWithComplexCondition; + +enum Bar +{ + + case ONE; + case TWO; + case THREE; + +} + +class Foo +{ + + public Bar $type; + + public function getRand(): int + { + return rand(0, 10); + } + + public function getPriority(): int + { + return match (true) { + $this->type === Bar::ONE => 0, + $this->type === Bar::TWO && $this->getRand() !== 8 => 1, + $this->type === BAR::THREE => 2, + $this->type === BAR::TWO => 3, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php index 3321e8929b..0d248e6c8e 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php @@ -21,4 +21,14 @@ public function doFoo( } } + /** @param array $strings */ + public function checkInArray(int $i, array $strings): void + { + if (in_array($i, $strings, true)) { + } + + if (in_array(1, $strings, true)) { + } + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php index cf6bcad137..e84f4f0901 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -181,6 +181,16 @@ public function doFoo() } } + public function doBar(Foo $foo) + { + if (method_exists($foo, 'test')) { + + } + if (method_exists($foo, 'doFoo')) { + + } + } + } final class FinalClassWithMethodExists @@ -198,6 +208,7 @@ public function doFoo() } +#[\AllowDynamicProperties] final class FinalClassWithPropertyExists { @@ -615,6 +626,21 @@ public function testWithNewObjectInFirstArgument(): void if (method_exists((new MethodExists()), $string)) { } } + + public function testWithTypehintedObject(MethodExists $methodExists): void + { + /** @var string $string */ + $string = doFoo(); + + if (method_exists($methodExists, 'testWithNewObjectInFirstArgument')) { + } + + if (method_exists($methodExists, 'undefinedMethod')) { + } + + if (method_exists($methodExists, $string)) { + } + } } trait MethodExistsTrait @@ -757,6 +783,7 @@ function doIpsum(array $data): void } +#[\AllowDynamicProperties] class Bug2221 { @@ -845,3 +872,132 @@ public function doBar($std, $stdClassesOrNull): void } } + +class ArraySearch +{ + + /** + * @param int $i + * @param non-empty-array $is + * @return void + */ + public function doFoo(int $i, array $is): void + { + $res = array_search($i, $is, true); + } + +} + +/** + * @phpstan-assert-if-true int $value + */ +function testIsInt(mixed $value): bool +{ + return is_int($value); +} + +function (int $int) { + if (testIsInt($int)) { + + } +}; + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif (is_int($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif (is_int($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } +} + +class InArray3 +{ + /** + * @param non-empty-array $nonEmptyInts + * @param array $strings + */ + public function doFoo(int $i, string $s, array $nonEmptyInts, array $strings): void + { + if (in_array($i, $strings)) { + } + + if (in_array($i, $strings, false)) { + } + + if (in_array(5, $strings)) { + } + + if (in_array(5, $strings, false)) { + } + + if (in_array($s, $nonEmptyInts)) { + } + + if (in_array($s, $nonEmptyInts, false)) { + } + + if (in_array('5', $nonEmptyInts)) { + } + + if (in_array('5', $nonEmptyInts, false)) { + } + + if (in_array(1, $strings, true)) { + } + } +} + +function checkSuperGlobals(): void +{ + foreach ($GLOBALS as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SERVER as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_GET as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_POST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_FILES as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_COOKIE as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SESSION as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_REQUEST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_ENV as $k => $v) { + if (is_int($k)) {} + } +} + +/** + * @param resource $resource + */ +function checkClosedResource($resource): void { + if (!is_resource($resource)) { + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php new file mode 100644 index 0000000000..761ac48b47 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php @@ -0,0 +1,47 @@ +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, + }; + } + + public function doNonEnum(bool $a): void + { + match ($a) { + true => 0, + false => 1, + }; + } + +} 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 index c56cfb8e34..baeec9922d 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -52,10 +52,6 @@ public function doFoo(int $i): void // unhandled }; - match ($i) { - // unhandled - }; - match ($i) { 1, 2 => null, default => null, // OK @@ -67,7 +63,7 @@ public function doFoo(int $i): void }; match (1) { - 1 => 1, // always true - report with strict-rules + 1 => 1, }; match ($i) { @@ -78,18 +74,22 @@ public function doFoo(int $i): void default => 1, 1 => 2, }; + + match ($i) { + // unhandled + }; } public function doBar(\Exception $e): void { match (true) { - $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule default => null, }; match (true) { $e instanceof \InvalidArgumentException => true, - $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule }; } @@ -138,3 +138,109 @@ public function doBar(int $i): void { } + +class ThrowsTag { + /** + * @throws \UnhandledMatchError + */ + public function foo(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } + + /** + * @throws \Error + */ + public function bar(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } + + /** + * @throws \Exception + */ + public function baz(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } +} + +function (): string { + $foo = fn(): int => rand(); + $bar = fn(): int => rand(); + return match ($foo <=> $bar) { + 1 => 'up', + 0 => 'neutral', + -1 => 'down', + }; +}; + +final class FinalFoo +{ + +} + +final class FinalBar +{ + +} + +class TestGetClass +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_class($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} +class TestGetDebugType +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_debug_type($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} + +class LastArm +{ + public const TYPE_A = 1; + public const TYPE_B = 2; + + + /** + * @param self::TYPE_* $type + */ + public function doMatch(int $type): void + { + match ($type) { + self::TYPE_A => 'A', + self::TYPE_B => 'B', + }; + + $day = date('N'); + match ($day) { + '1' => 'Mon', + '2' => 'Tue', + '3' => 'Wed', + '4' => 'Thu', + '5' => 'Fri', + '6' => 'Sat', + '7' => 'Sun', + }; + } +} 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/pr-4375.php b/tests/PHPStan/Rules/Comparison/data/pr-4375.php new file mode 100644 index 0000000000..26df1dbfa4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/pr-4375.php @@ -0,0 +1,27 @@ +get() 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; + } + } + + return []; + } + + private function get(): array { + return []; + } +} 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 { @@ -974,3 +974,80 @@ public function doBar(array $a): void } } + +function () { + INF === INF; + NAN === NAN; + + INF !== INF; + NAN !== NAN; +}; + +class ArrayWithLongStrings2 +{ + + public function doFoo() + { + $array = ['foofoofoofoofoofoofoo','foofoofoofoofoofoofob']; + + foreach ($array as $value) { + if ('foofoofoofoofoofoofoo' === $value) { + echo 'nope'; + } elseif ('foofoofoofoofoofoofob' === $value) { + echo 'nop nope'; + } elseif (rand(0, 1) === 0) { + echo 'nope'; + } + } + } + +} + +class TestLiteralStringVerbosityFix +{ + + /** + * @param lowercase-string|false $a + */ + public function doFoo($a): void + { + if ($a === 'AB') { + + } + } + +} + +class SubtractedMixedAgainstNull +{ + + public function doFoo($m): void + { + if ($m === null) { + return; + } + + if ($m === null) { + + } + + if ($m !== null) { + + } + } + + public function doBar($m, int $i, int $j): void + { + if ($m === null) { + return; + } + + $a = [1, $m, 3]; + $b = [$i, null, $j]; + + if ($a !== $b) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/ternary.php b/tests/PHPStan/Rules/Comparison/data/ternary.php index e2c3048ec4..b693f44656 100644 --- a/tests/PHPStan/Rules/Comparison/data/ternary.php +++ b/tests/PHPStan/Rules/Comparison/data/ternary.php @@ -1,6 +1,6 @@ + */ +class ClassAsClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ClassAsClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-as-class-constant.php'], [ + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 9, + ], + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/ConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ConstantRuleTest.php index cef571f595..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(true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool { - return new ConstantRule(); + return true; } public function testConstants(): void @@ -25,14 +34,10 @@ public function testConstants(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Constant DEFINED_CONSTANT not found.', - 13, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - /*[ 'Constant DEFINED_CONSTANT_IF not found.', 21, - ],*/ + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], ]); } @@ -63,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 index 924992ae73..861c72d7ab 100644 --- a/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php +++ b/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php @@ -5,15 +5,11 @@ class DirectAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { - /** @var AlwaysUsedClassConstantsExtension[] */ - private $extensions; - /** * @param AlwaysUsedClassConstantsExtension[] $extensions */ - public function __construct(array $extensions) + public function __construct(private array $extensions) { - $this->extensions = $extensions; } /** diff --git a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php new file mode 100644 index 0000000000..5d829849db --- /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(self::createReflectionProvider(), true, false, true, false, false, false, true), + ); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 15, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 16, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 18, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 19, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 20, + ], + ]; + } else { + $errors = [ + [ + 'Class constant name in dynamic fetch can only be a string, int given.', + 18, + ], + [ + 'Class constant name in dynamic fetch can only be a string, int|string given.', + 19, + ], + [ + 'Class constant name in dynamic fetch can only be a string, string|null given.', + 20, + ], + ]; + } + $this->analyse([__DIR__ . '/data/dynamic-class-constant-fetch.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php index df7f57f92a..3f0430b2b4 100644 --- a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -12,15 +13,14 @@ class FinalConstantRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId; + private int $phpVersionId; protected function getRule(): Rule { return new FinalConstantRule(new PhpVersion($this->phpVersionId)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -40,16 +40,11 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/final-constant.php'], $errors); } diff --git a/tests/PHPStan/Rules/Constants/FinalPrivateConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalPrivateConstantRuleTest.php new file mode 100644 index 0000000000..8e98e04565 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/FinalPrivateConstantRuleTest.php @@ -0,0 +1,27 @@ + */ +class FinalPrivateConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/final-private-const.php'], [ + [ + 'Private constant FinalPrivateConstants\User::FINAL_PRIVATE() cannot be final as it is never overridden by other classes.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php new file mode 100644 index 0000000000..2f598d78b3 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php @@ -0,0 +1,167 @@ + + */ +class MagicConstantContextRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MagicConstantContextRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/magic-constant.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 59, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 64, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 78, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 91, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 92, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 93, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 97, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 101, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 105, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 109, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 115, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 120, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 133, + ], + ]); + } + + public function testGlobalNamespace(): void + { + $this->analyse([__DIR__ . '/data/magic-constant-global-ns.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 8, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 16, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 25, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 34, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 46, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 48, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 51, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php index f084566139..2c95dbb317 100644 --- a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Constants; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -11,10 +13,9 @@ class MissingClassConstantTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - return new MissingClassConstantTypehintRule(new MissingTypehintCheck($reflectionProvider, true, true, true)); + return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -28,7 +29,6 @@ public function testRule(): void [ 'Constant MissingClassConstantTypehint\Foo::BAZ with generic class MissingClassConstantTypehint\Bar does not specify its types: T', 17, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Constant MissingClassConstantTypehint\Foo::LOREM type has no signature specified for callable.', @@ -37,4 +37,26 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testBug8957(): void + { + $this->analyse([__DIR__ . '/data/bug-8957.php'], []); + } + + #[RequiresPhp('>= 8.3')] + public function testRuleShouldNotApplyToNativeTypes(): void + { + $this->analyse([__DIR__ . '/data/class-constant-native-type.php'], [ + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::B type has no value type specified in iterable type array.', + 19, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::D with generic class ClassConstantNativeTypeForMissingTypehintRule\Bar does not specify its types: T', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php new file mode 100644 index 0000000000..7e0e4e6103 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php @@ -0,0 +1,36 @@ + + */ +class NativeTypedClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new NativeTypedClassConstantRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Class constants with native types are supported only on PHP 8.3 and later.', + 10, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/native-typed-class-constant.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php index 24eaee4c4a..5e14d98461 100644 --- a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Constants; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** @extends RuleTestCase */ class OverridingConstantRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new OverridingConstantRule(true); } @@ -29,10 +32,6 @@ public function testRule(): void public function testFinal(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $errors = [ [ 'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.', @@ -92,4 +91,27 @@ public function testFinal(): void $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); } + #[RequiresPhp('>= 8.3')] + public function testNativeTypes(): void + { + $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..5405390a90 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -0,0 +1,97 @@ + + */ +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'], []); + } + + #[RequiresPhp('>= 8.3')] + public function testNativeType(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testBug10212(): void + { + $this->analyse([__DIR__ . '/data/bug-10212.php'], [ + [ + 'Constant Bug10212\HelloWorld::B (Bug10212\X\Foo) does not accept value Bug10212\Foo::Bar.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-10212.php b/tests/PHPStan/Rules/Constants/data/bug-10212.php new file mode 100644 index 0000000000..40fda3454c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-10212.php @@ -0,0 +1,22 @@ += 8.3 + +namespace Bug10212; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + case Bar; +} + +class HelloWorld +{ + public const string A = 'foo'; + public const X\Foo B = Foo::Bar; + public const Foo C = Foo::Bar; +} + +function(HelloWorld $hw): void { + assertType(X\Foo::class, $hw::B); + assertType(Foo::class, $hw::C); +}; diff --git a/tests/PHPStan/Rules/Constants/data/bug-5655.php b/tests/PHPStan/Rules/Constants/data/bug-5655.php new file mode 100644 index 0000000000..058d25136f --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-5655.php @@ -0,0 +1,14 @@ + '', + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273.php b/tests/PHPStan/Rules/Constants/data/bug-7273.php new file mode 100644 index 0000000000..b21ffc9d58 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273.php @@ -0,0 +1,75 @@ + + */ +abstract class SomeValueProcessor implements ConfigValueProcessorInterface +{ +} + +interface ConfigKey +{ + public const ACTIVITY__EXPORT__TYPE = 'activity.export.type'; + public const ACTIVITY__TAGS__MULTI = 'activity.tags.multi'; + // ... +} + +interface Module +{ + public const ABSENCEREQUEST = 'absencerequest'; + public const ACTIVITY = 'activity'; + // ... +} + +interface ConfigRepositoryInterface +{ + /** + * @var array>, mixed>, + * }> + */ + public const CONFIGURATIONS = [ + ConfigKey::ACTIVITY__EXPORT__TYPE => [ + 'type' => "'SomeExport'|'SomeOtherExport'|null", + 'default' => null, + 'acl' => ['superadmin'], + 'linked_module' => Module::ACTIVITY, + ], + ConfigKey::ACTIVITY__TAGS__MULTI => [ + 'default' => false, + 'acl' => ['admin'], + 'linked_module' => Module::ACTIVITY, + 'value_processors' => [ + SomeValueProcessor::class => ['someOption' => true], + ], + ], + // ... + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273b.php b/tests/PHPStan/Rules/Constants/data/bug-7273b.php new file mode 100644 index 0000000000..136efa9a9e --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273b.php @@ -0,0 +1,51 @@ + */ + public const FIRST_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array */ + public const SECOND_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array{url?: string, title: string}[] */ + public const THIRD_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php b/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php new file mode 100644 index 0000000000..a9b87c2cf7 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php @@ -0,0 +1,11 @@ += 8.2 + +namespace Bug8957; + +use function PHPStan\Testing\assertType; + +enum A: string +{ + case X = 'x'; + case Y = 'y'; +} + +class B { + public const A = [ + A::X->value, + A::Y->value, + ]; + + public function doFoo(): void + { + assertType('array{\'x\', \'y\'}', self::A); + } +} diff --git a/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php new file mode 100644 index 0000000000..fdf86f77f1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php @@ -0,0 +1,17 @@ += 8.3 + +namespace ClassConstantNativeTypeForMissingTypehintRule; + +/** @template T */ +class Bar +{ + +} + +const ConstantWithObjectInstanceForNativeType = new Bar(); + +class Foo +{ + + public const array A = []; + + /** @var array */ + public const array B = []; + + public const Bar C = ConstantWithObjectInstanceForNativeType; + + /** @var Bar */ + public const Bar D = ConstantWithObjectInstanceForNativeType; +} diff --git a/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php new file mode 100644 index 0000000000..9209ddf238 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php @@ -0,0 +1,11 @@ += 8.3 + +namespace NativeTypedClassConstant; + +class Foo +{ + + public const TEST = 1; + + public const int LOREM = 2; + +} diff --git a/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php new file mode 100644 index 0000000000..9b8afbbcad --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php @@ -0,0 +1,50 @@ += 8.3 + +namespace OverridingConstantNativeTypes; + +class Foo +{ + + public const int A = 1; + public const int|string B = 1; + public const int|string C = 1; + public const int D = 1; + +} + +class Bar extends Foo +{ + + public const int A = 2; + public const int|string B = 'foo'; + public const int C = 1; + public const int|string D = 1; + +} + +class Lorem +{ + + public const A = 1; + public const int B = 1; + +} + +class Ipsum extends Lorem +{ + + public const int A = 1; + public const B = 1; + +} + +class PharChild extends \Phar +{ + + const BZ2 = 'foo'; // error + + const int GZ = 1; // OK + + const int|string NONE = 1; // error + +} diff --git a/tests/PHPStan/Rules/Constants/data/overriding-final-constant.php b/tests/PHPStan/Rules/Constants/data/overriding-final-constant.php index 50deb4b5a6..5eb27265af 100644 --- a/tests/PHPStan/Rules/Constants/data/overriding-final-constant.php +++ b/tests/PHPStan/Rules/Constants/data/overriding-final-constant.php @@ -1,4 +1,4 @@ -= 8.1 += 8.3 + +namespace ValueAssignedToClassConstantNativeType; + +class Foo +{ + + public const int FOO = 1; + + public const int BAR = 'bar'; + +} + +class Bar +{ + + /** @var int<1, max> */ + public const int FOO = 1; + + /** @var int<1, max> */ + public const int BAR = 0; + +} + +class Floats +{ + + public const float FOO = 1; + + public const int BAR = 1.0; + + /** @var float */ + public const BAZ = 1; + + /** @var float */ + public const float LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php new file mode 100644 index 0000000000..bdcf81838c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php @@ -0,0 +1,49 @@ + */ + const DOLOR = 1; + +} + +class Bar extends Foo +{ + + const BAR = 2; + + const BAZ = 2; + +} + +class Baz +{ + + /** @var string */ + private const BAZ = 'foo'; + +} + +class Lorem extends Baz +{ + + private const BAZ = 1; + +} diff --git a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php index 6bd57a85c0..40711affec 100644 --- a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php @@ -2,13 +2,15 @@ namespace PHPStan\Rules; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DateTimeInstantiationRuleTest extends \PHPStan\Testing\RuleTestCase +class DateTimeInstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DateTimeInstantiationRule(); } @@ -42,7 +44,15 @@ public function test(): void 'Instantiating DateTime with 2020-04-31 produces a warning: The parsed date was invalid', 20, ],*/ - ] + [ + 'Instantiating DateTime with 2020.11.17 produces an error: Double time specification', + 22, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.17 produces an error: Double time specification', + 23, + ], + ], ); } diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..c4dd2583f3 --- /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(self::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..6fc162edc6 --- /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(self::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..e0bd3d2168 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,103 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11011(): void + { + $this->analyse([__DIR__ . '/data/bug-11011.php'], [ + [ + 'Call to method Bug11011\AnotherPureImpl::doFoo() on a separate line has no effect.', + 32, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12379(): void + { + $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 23a83440e5..5750c03c3e 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,15 +77,42 @@ public function testRule(): void 'Expression "(string) 1" on a separate line does not do anything.', 30, ], + [ + 'Unused result of "xor" operator.', + 32, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "and" operator.', + 35, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "or" operator.', + 38, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of ternary operator.', + 40, + ], + [ + 'Unused result of ternary operator.', + 41, + ], + [ + 'Unused result of "||" operator.', + 46, + ], + [ + 'Unused result of "&&" operator.', + 49, + ], ]); } public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [ [ 'Expression "$ref?->name" on a separate line does not do anything.', @@ -93,4 +121,41 @@ public function testNullsafe(): void ]); } + public function testRuleImpurePoints(): void + { + $this->analyse([__DIR__ . '/data/noop-impure-points.php'], [ + [ + 'Unused result of "&&" operator.', + 12, + ], + [ + 'Expression "$b()" on a separate line does not do anything.', + 59, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 98, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 104, + ], + ]); + } + + public function testBug11001(): void + { + $this->analyse([__DIR__ . '/data/bug-11001.php'], []); + } + + public function testBug11361(): void + { + $this->analyse([__DIR__ . '/data/bug-11361.php'], []); + } + + public function testBug13067(): void + { + $this->analyse([__DIR__ . '/data/bug-13067.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 a3c2fb7db2..e79a185935 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -4,15 +4,16 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnreachableStatementRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -42,7 +43,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, ], ]); } @@ -58,7 +71,7 @@ public function testRuleTopLevel(): void ]); } - public function dataBugWithoutGitHubIssue1(): array + public static function dataBugWithoutGitHubIssue1(): array { return [ [ @@ -70,10 +83,7 @@ public function dataBugWithoutGitHubIssue1(): array ]; } - /** - * @dataProvider dataBugWithoutGitHubIssue1 - * @param bool $treatPhpDocTypesAsCertain - */ + #[DataProvider('dataBugWithoutGitHubIssue1')] public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -122,4 +132,245 @@ public function testBug4370(): void $this->analyse([__DIR__ . '/data/bug-4370.php'], []); } + public function testBug7188(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7188.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 22, + ], + ]); + } + + public function testBug8620(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8620.php'], []); + } + + public function testBug4002(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002.php'], []); + } + + public function testBug4002Two(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-2.php'], []); + } + + public function testBug4002Three(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-3.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 13, + ], + ]); + } + + public function testBug4002Four(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-4.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 9, + ], + ]); + } + + public function testBug4002Class(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_class.php'], []); + } + + public function testBug4002Interface(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_interface.php'], []); + } + + public function testBug4002Trait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_trait.php'], []); + } + + public function testBug8319(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8319.php'], []); + } + + public function testBug8966(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8966.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 8, + ], + ]); + } + + public function testBug11179(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11179.php'], []); + } + + public function testBug11992(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11992.php'], []); + } + + public function testBug7531(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7531.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 22, + ], + ]); + } + + public function testMultipleUnreachable(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 14, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11909(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11909.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13232a(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + [ + 'Unreachable statement - code above always terminates.', + 17, + ], + [ + 'Unreachable statement - code above always terminates.', + 23, + ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], + [ + 'Unreachable statement - code above always terminates.', + 38, + ], + [ + 'Unreachable statement - code above always terminates.', + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 52, + ], + [ + 'Unreachable statement - code above always terminates.', + 61, + ], + [ + 'Unreachable statement - code above always terminates.', + 70, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13232b(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13232c(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 12, + ], + [ + 'Unreachable statement - code above always terminates.', + 20, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13232d(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 11, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13288(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13288.php'], []); + } + + public function testBug13311(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13311.php'], []); + } + + public function testBug13307(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13307.php'], []); + } + + public function testBug13331(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13331.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index 305c37cf13..79e15a843c 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -2,11 +2,12 @@ namespace PHPStan\Rules\DeadCode; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension; use PHPStan\Rules\Constants\DirectAlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use UnusedPrivateConstant\TestExtension; /** @@ -21,14 +22,14 @@ protected function getRule(): Rule new DirectAlwaysUsedClassConstantsExtensionProvider([ new class() implements AlwaysUsedClassConstantsExtension { - public function isAlwaysUsed(ConstantReflection $constant): bool + public function isAlwaysUsed(ClassConstantReflection $constant): bool { return $constant->getDeclaringClass()->getName() === TestExtension::class && $constant->getName() === 'USED'; } }, - ]) + ]), ); } @@ -38,21 +39,65 @@ public function testRule(): void [ 'Constant UnusedPrivateConstant\Foo::BAR_CONST is unused.', 10, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', ], [ 'Constant UnusedPrivateConstant\TestExtension::UNUSED is unused.', 23, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', ], ]); } public function testBug5651(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-5651.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8204(): void + { + $this->analyse([__DIR__ . '/data/bug-8204.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9005(): void + { + $this->analyse([__DIR__ . '/data/bug-9005.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + #[RequiresPhp('>= 8.3')] + public function testDynamicConstantFetch(): void + { + $this->analyse([__DIR__ . '/data/unused-private-constant-dynamic-fetch.php'], [ + [ + 'Constant UnusedPrivateConstantDynamicFetch\Baz::FOO is unused.', + 32, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index b06a4c013d..093d372358 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\DeadCode; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtension; +use PHPStan\Rules\Methods\DirectAlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -13,7 +17,19 @@ class UnusedPrivateMethodRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedPrivateMethodRule(); + return new UnusedPrivateMethodRule( + new DirectAlwaysUsedMethodExtensionProvider([ + new class() implements AlwaysUsedMethodExtension { + + public function isAlwaysUsed(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->is('UnusedPrivateMethod\IgnoredByExtension') + && $methodReflection->getName() === 'foo'; + } + + }, + ]), + ); } public function testRule(): void @@ -37,7 +53,11 @@ public function testRule(): void ], [ 'Method UnusedPrivateMethod\Lorem::doBaz() is unused.', - 97, + 99, + ], + [ + 'Method UnusedPrivateMethod\IgnoredByExtension::bar() is unused.', + 181, ], ]); } @@ -49,11 +69,69 @@ public function testBug3630(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-unused-private-method.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testFirstClassCallable(): void + { + $this->analyse([__DIR__ . '/data/callable-unused-private-method.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testFalsePositiveWithTraitUse(): void + { + $this->analyse([__DIR__ . '/data/unused-method-false-positive-with-trait.php'], []); + } + + public function testBug6039(): void + { + $this->analyse([__DIR__ . '/data/bug-6039.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testBug11802(): void + { + $this->analyse([__DIR__ . '/data/bug-11802b.php'], [ + [ + 'Method Bug11802b\HelloWorld::doBar() is unused.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 0df99db626..199e22d1a2 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -7,7 +7,8 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function in_array; /** * @extends RuleTestCase @@ -16,10 +17,12 @@ class UnusedPrivatePropertyRuleTest extends RuleTestCase { /** @var string[] */ - private $alwaysWrittenTags; + private array $alwaysWrittenTags; /** @var string[] */ - private $alwaysReadTags; + private array $alwaysReadTags; + + private bool $checkUninitializedProperties = false; protected function getRule(): Rule { @@ -54,73 +57,105 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]), $this->alwaysWrittenTags, $this->alwaysReadTags, - true + $this->checkUninitializedProperties, ); } public function testRule(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4 or static reflection.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; + + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/unused-private-property.php'], [ [ 'Property UnusedPrivateProperty\Foo::$bar is never read, only written.', 10, + $tip, ], [ 'Property UnusedPrivateProperty\Foo::$baz is unused.', 12, + $tip, ], [ 'Property UnusedPrivateProperty\Foo::$lorem is never written, only read.', 14, + $tip, ], [ 'Property UnusedPrivateProperty\Bar::$baz is never written, only read.', 57, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$bar is never read, only written.', 86, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$baz is unused.', 88, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$lorem is never written, only read.', 90, + $tip, ], [ 'Property UnusedPrivateProperty\Lorem::$baz is never read, only written.', 117, + $tip, ], [ 'Property class@anonymous/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php:152::$bar is unused.', 153, + $tip, ], [ 'Property UnusedPrivateProperty\DolorWithAnonymous::$foo is unused.', 148, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ArrayAssign::$foo is never read, only written.', + 162, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ListAssign::$foo is never read, only written.', + 191, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection1 is never read, only written.', + 221, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection2 is never read, only written.', + 224, + $tip, ], ]); $this->analyse([__DIR__ . '/data/TestExtension.php'], [ [ 'Property UnusedPrivateProperty\TestExtension::$unused is unused.', 8, + $tip, ], [ 'Property UnusedPrivateProperty\TestExtension::$read is never written, only read.', 10, + $tip, ], [ 'Property UnusedPrivateProperty\TestExtension::$written is never read, only written.', 12, + $tip, ], ]); } @@ -129,14 +164,17 @@ public function testAlwaysUsedTags(): void { $this->alwaysWrittenTags = ['@ORM\Column']; $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/private-property-with-tags.php'], [ [ 'Property PrivatePropertyWithTags\Foo::$title is never read, only written.', 13, + $tip, ], [ 'Property PrivatePropertyWithTags\Foo::$text is never written, only read.', 18, + $tip, ], ]); } @@ -150,44 +188,226 @@ public function testTrait(): void public function testBug3636(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/bug-3636.php'], [ [ 'Property Bug3636\Bar::$date is never written, only read.', 22, + $tip, ], ]); } public function testPromotedProperties(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/unused-private-promoted-property.php'], [ [ 'Property UnusedPrivatePromotedProperty\Foo::$lorem is never read, only written.', 12, + $tip, ], ]); } public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/nullsafe-unused-private-property.php'], []); } + public function testBug3654(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-3654.php'], []); + } + + public function testBug5935(): void + { + $this->alwaysWrittenTags = []; + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8204(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10059(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10059.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10628(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12621(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + + $this->analyse([__DIR__ . '/data/bug-12621.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12702(): void + { + $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, + ], + ]); + } + + public function testBug9213(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + + $this->analyse([__DIR__ . '/data/bug-9213.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10059.php b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php new file mode 100644 index 0000000000..fdc6335e27 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug10059; + +use DateTimeImmutable; + +class Foo +{ + public function __construct( + private readonly DateTimeImmutable $startDateTime + ) { + } + + public function bar(): void + { + declare(ticks=5) { + echo $this->startDateTime->format('Y-m-d H:i:s.u'), PHP_EOL; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10628.php b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php new file mode 100644 index 0000000000..3fee75ba1e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug10628; + +use stdClass; + +interface Bar +{ + + public function bazName(): string; + +} + +final class Foo +{ + public function __construct( + private Bar $bar, + ) { + } + + public function __invoke(): stdClass + { + return $this->getMixed()->get( + name: $this->bar->bazName(), + ); + } + + public function getMixed(): mixed + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-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-11909.php b/tests/PHPStan/Rules/DeadCode/data/bug-11909.php new file mode 100644 index 0000000000..3b83603011 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11909.php @@ -0,0 +1,10 @@ +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-13067.php b/tests/PHPStan/Rules/DeadCode/data/bug-13067.php new file mode 100644 index 0000000000..76386890bc --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13067.php @@ -0,0 +1,30 @@ +neverReturnsMethod()); + echo 'this will never happen'; + } + + public function sayHi(): void + { + echo 'Hello, ' . neverReturns() + . ' no way'; + echo 'this will never happen'; + } + + public function sayHo(): void + { + echo "Hello, {$this->neverReturnsMethod()} no way"; + echo 'this will never happen'; + } + + public function sayHe(): void + { + $callable = function (): never { + exit(); + }; + echo sprintf("Hello, %s no way", $callable()); + echo 'this will never happen'; + } + + public function sayHe2(): void + { + $this->doFoo($this->neverReturnsMethod()); + echo 'this will never happen'; + } + + public function sayHe3(): void + { + self::doStaticFoo($this->neverReturnsMethod()); + echo 'this will never happen'; + } + + public function sayHuu(): void + { + $x = [ + $this->neverReturnsMethod() + ]; + echo 'this will never happen'; + } + + public function sayClosure(): void + { + $callable = function (): never { + exit(); + }; + $callable(); + echo 'this will never happen'; + } + + public function sayIIFE(): void + { + (function (): never { + exit(); + })(); + + echo 'this will never happen'; + } + + function neverReturnsMethod(): never { + exit(); + } + + public function doFoo() {} + + static public function doStaticFoo() {} +} +function neverReturns(): never { + exit(); +} + diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php new file mode 100644 index 0000000000..9818fb7849 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug13232c; + +final class HelloWorld +{ + public function sayHello(): void + { + echo 'Hello, ' . $this->returnNever() + . ' no way'; + + echo 'this will never happen'; + } + + static public function sayStaticHello(): void + { + echo 'Hello, ' . self::staticReturnNever() + . ' no way'; + + echo 'this will never happen'; + } + + public function sayNullsafeHello(?self $x): void + { + echo 'Hello, ' . $x?->returnNever() + . ' no way'; + + echo 'this might happen, in case $x is null'; + } + + public function sayMaybeHello(): void + { + if (rand(0, 1)) { + echo 'Hello, ' . $this->returnNever() + . ' no way'; + } + + echo 'this might happen'; + } + + function returnNever(): never + + { + exit(); + } + + static function staticReturnNever(): never + { + exit(); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php new file mode 100644 index 0000000000..89c58d0723 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug13288; + +function error_to_exception(int $errno, string $errstr, string $errfile = 'unknown', int $errline = 0): never { + throw new \ErrorException($errstr, $errno, $errno, $errfile, $errline); +} + +set_error_handler(error_to_exception(...)); + +echo 'ok'; diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13307.php b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php new file mode 100644 index 0000000000..78dc84dd4b --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php @@ -0,0 +1,24 @@ += 7.4 +id = $id; + } + + public function jsonSerialize(): array + { + return \get_object_vars($this); + } +} + +class Bar implements \JsonSerializable +{ + + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function jsonSerialize(): void + { + \array_walk($this, static function ($key, $value) { + }); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php new file mode 100644 index 0000000000..f16716af43 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php @@ -0,0 +1,11 @@ +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-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-7531.php b/tests/PHPStan/Rules/DeadCode/data/bug-7531.php new file mode 100644 index 0000000000..e30e32d60e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-7531.php @@ -0,0 +1,24 @@ + + + + $compareTo) : ?> + some xml data + + + + + + + $compareTo) : ?> + some xml data + + + + + 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-9213.php b/tests/PHPStan/Rules/DeadCode/data/bug-9213.php new file mode 100644 index 0000000000..bd445e70f1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9213.php @@ -0,0 +1,22 @@ += 8.0 + +namespace Bug9213; + +class IterateSomething +{ + /** @var string|null */ + private $currentKey; + + /** @var string|null */ + private $currentValue; + + /** @param iterable $collection */ + public function __construct(private iterable $collection) {} + + public function printAllTheThings(): void + { + foreach ($this->collection as $this->currentKey => $this->currentValue) { + echo "{$this->currentKey} => {$this->currentValue}\n"; + } + } +} 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/property-hooks-unused-property.php b/tests/PHPStan/Rules/DeadCode/data/property-hooks-unused-property.php new file mode 100644 index 0000000000..6b856eb132 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/property-hooks-unused-property.php @@ -0,0 +1,112 @@ += 8.4 + +namespace PropertyHooksUnusedProperty; + +class FooUsed +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function setA(int $a): void + { + $this->a = $a; + } + + public function getA(): int + { + return $this->a; + } + +} + +class FooUnused +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + +} + +class FooOnlyRead +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function getA(): int + { + return $this->a; + } + +} + +class FooOnlyWritten +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function setA(int $a): void + { + $this->a = $a; + } + +} + +class ReadInAnotherPropertyHook +{ + public function __construct( + private readonly string $bar, + ) {} + + public string $virtualProperty { + get => $this->bar; + } +} + +class ReadInAnotherPropertyHook2 +{ + + private string $bar; + + public string $virtualProperty { + get => $this->bar; + } +} + +class WrittenInAnotherPropertyHook +{ + + private string $bar; + + public string $virtualProperty { + set { + $this->bar = 'test'; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unreachable.php b/tests/PHPStan/Rules/DeadCode/data/unreachable.php index 69b6c3c8bc..48f39153f1 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unreachable.php +++ b/tests/PHPStan/Rules/DeadCode/data/unreachable.php @@ -36,6 +36,28 @@ public function doLorem() // this is why... } + public function doLorem2(string $foo) + { + return; + // this is why... + + echo $foo; + } + + public function doLorem3() + { + return; + ; + } + + public function doLorem4(string $foo) + { + return; + ; + + echo $foo; + } + /** * @param \stdClass[] $all */ @@ -114,3 +136,25 @@ private function somethingAboutDateTime(\DateTime $dt): bool } } + +class LastElseIf +{ + + /** + * @param 'a'|'b'|'c' $s + * @return void + */ + public function doFoo(string $s): void + { + if ($s === 'a') { + return; + } elseif ($s === 'b') { + return; + } elseif ($s === 'c') { + return; + } + + echo "test"; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php new file mode 100644 index 0000000000..24e2c3256d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php @@ -0,0 +1,72 @@ += 8.1 + +namespace UnusedMethodFalsePositiveWithTrait; + +use ReflectionEnum; + +enum LocalOnlineReservationTime: string +{ + + use LabeledEnumTrait; + + case MORNING = 'morning'; + case AFTERNOON = 'afternoon'; + case EVENING = 'evening'; + + public static function getPeriodForHour(string $hour): self + { + $hour = self::hourToNumber($hour); + + throw new \Exception('Internal error'); + } + + private static function hourToNumber(string $hour): int + { + return (int) str_replace(':', '', $hour); + } + +} + +trait LabeledEnumTrait +{ + + use EnumTrait; + +} + +trait EnumTrait +{ + + /** + * @return list + */ + public static function getDeprecatedEnums(): array + { + static $cache = []; + if ($cache === []) { + $reflection = new ReflectionEnum(self::class); + $cases = $reflection->getCases(); + + foreach ($cases as $case) { + $docComment = $case->getDocComment(); + if ($docComment === false || !str_contains($docComment, '@deprecated')) { + continue; + } + $cache[] = self::from($case->getBackingValue()); + } + } + + return $cache; + } + + public function isDeprecated(): bool + { + return $this->equalsAny(self::getDeprecatedEnums()); + } + + public function equalsAny(...$that): bool + { + return in_array($this, $that, true); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php new file mode 100644 index 0000000000..089efcb892 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php @@ -0,0 +1,39 @@ += 8.3 + +namespace UnusedPrivateConstantDynamicFetch; + +class Foo +{ + + private const FOO = 1; + + public function doFoo(string $s): void + { + echo self::{$s}; + } + +} + +class Bar +{ + + private const FOO = 1; + + public function doFoo(self $a, string $s): void + { + echo $a::{$s}; + } + +} + +class Baz +{ + + private const FOO = 1; + + public function doFoo(\stdClass $a, string $s): void + { + echo $a::{$s}; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-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-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 index be6f11bdd0..ce43e40a90 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php @@ -80,8 +80,10 @@ private function doFoo() public function doBar(string $name) { - $cb = [$this, $name]; - $cb(); + if ($name === 'doFoo') { + $cb = [$this, $name]; + $cb(); + } } } @@ -154,3 +156,29 @@ private function doLorem() } } + +class StaticMethod +{ + + private static function doFoo(): void + { + + } + + public function doTest(): void + { + $this::doFoo(); + } + +} + +class IgnoredByExtension +{ + private function foo(): void + { + } + + private function bar(): void + { + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php index 3511bee8a9..97aa923a37 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php @@ -1,4 +1,4 @@ -= 7.4 +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..90a5dd508c --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php @@ -0,0 +1,56 @@ + + */ +class DebugScopeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DebugScopeRule(self::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/DumpNativeTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpNativeTypeRuleTest.php new file mode 100644 index 0000000000..71aba72e45 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DumpNativeTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class DumpNativeTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DumpNativeTypeRule(self::createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/dump-native-type.php'], [ + [ + 'Dumped type: non-empty-array', + 11, + ], + [ + 'Dumped type: array', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..8b137b1611 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php @@ -0,0 +1,106 @@ + + */ +class DumpPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DumpPhpDocTypeRule(self::createReflectionProvider(), new Printer()); + } + + public function testRuleSymbols(): void + { + $this->analyse([__DIR__ . '/data/dump-phpdoc-type.php'], [ + [ + "Dumped type: array{'': ''}", + 5, + ], + [ + "Dumped type: array{'\0': 'NUL', NUL: '\0'}", + 6, + ], + [ + "Dumped type: array{'\001': 'SOH', SOH: '\001'}", + 7, + ], + [ + "Dumped type: array{'\t': 'HT', HT: '\t'}", + 8, + ], + [ + "Dumped type: array{' ': 'SP', SP: ' '}", + 11, + ], + [ + "Dumped type: array{'foo ': 'ends with SP', ' foo': 'starts with SP', ' foo ': 'surrounded by SP', foo: 'no SP'}", + 12, + ], + [ + "Dumped type: array{'foo?': 'foo?'}", + 15, + ], + [ + "Dumped type: array{shallwedance: 'yes'}", + 16, + ], + [ + "Dumped type: array{'shallwedance?': 'yes'}", + 17, + ], + [ + "Dumped type: array{'Shall we dance': 'yes'}", + 18, + ], + [ + "Dumped type: array{'Shall we dance?': 'yes'}", + 19, + ], + [ + "Dumped type: array{shall_we_dance: 'yes'}", + 20, + ], + [ + "Dumped type: array{'shall_we_dance?': 'yes'}", + 21, + ], + [ + "Dumped type: array{shall-we-dance: 'yes'}", + 22, + ], + [ + "Dumped type: array{'shall-we-dance?': 'yes'}", + 23, + ], + [ + "Dumped type: array{'Let\'s go': 'Let\'s go'}", + 24, + ], + [ + "Dumped type: array{Foo\\Bar: 'Foo\\\\Bar'}", + 25, + ], + [ + "Dumped type: array{'3.14': 3.14}", + 26, + ], + [ + 'Dumped type: array{1: true, 0: false}', + 27, + ], + [ + 'Dumped type: T', + 36, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php index f172d91e98..fb4c489dea 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -13,20 +13,16 @@ class DumpTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DumpTypeRule($this->createReflectionProvider()); + return new DumpTypeRule(self::createReflectionProvider()); } public function testRuleInPhpStanNamespace(): void { $this->analyse([__DIR__ . '/data/dump-type.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 10, ], - [ - 'Missing argument for PHPStan\dumpType() function call.', - 11, - ], ]); } @@ -34,7 +30,7 @@ public function testRuleInDifferentNamespace(): void { $this->analyse([__DIR__ . '/data/dump-type-ns.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 10, ], ]); @@ -44,14 +40,66 @@ public function testRuleInUse(): void { $this->analyse([__DIR__ . '/data/dump-type-use.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 12, ], [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 13, ], ]); } + public function testBug7803(): void + { + $this->analyse([__DIR__ . '/data/bug-7803.php'], [ + [ + 'Dumped type: int<4, max>', + 11, + ], + [ + 'Dumped type: non-empty-array', + 12, + ], + [ + 'Dumped type: int<4, max>', + 13, + ], + ]); + } + + public function testBug10377(): void + { + $this->analyse([__DIR__ . '/data/bug-10377.php'], [ + [ + 'Dumped type: array', + 22, + ], + [ + 'Dumped type: array', + 34, + ], + ]); + } + + public function testBug11179(): void + { + $this->analyse([__DIR__ . '/../DeadCode/data/bug-11179.php'], [ + [ + 'Dumped type: string', + 9, + ], + ]); + } + + public function testBug11179NoNamespace(): void + { + $this->analyse([__DIR__ . '/data/bug-11179-no-namespace.php'], [ + [ + 'Dumped type: string', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php index bf1ea7711f..c58a8de72b 100644 --- a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php +++ b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Debug; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -13,7 +14,10 @@ class FileAssertRuleTest extends RuleTestCase protected function getRule(): Rule { - return new FileAssertRule($this->createReflectionProvider()); + return new FileAssertRule( + self::createReflectionProvider(), + self::getContainer()->getByType(TypeStringResolver::class), + ); } public function testRule(): void @@ -21,23 +25,39 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/file-asserts.php'], [ [ 'Expected type array, actual: array', - 19, + 20, + ], + [ + 'Expected subtype of array, actual: array', + 23, ], [ 'Expected native type false, actual: bool', - 36, + 41, ], [ 'Expected native type true, actual: bool', - 37, + 42, + ], + [ + 'Expected subtype of string, actual: false', + 47, + ], + [ + 'Expected subtype of never, actual: false', + 48, + ], + [ + 'Expected variable $b certainty Yes, actual: No', + 56, ], [ - 'Expected variable certainty Yes, actual: No', - 45, + 'Expected variable $b certainty Maybe, actual: No', + 57, ], [ - 'Expected variable certainty Maybe, actual: No', - 46, + "Expected offset 'firstName' certainty No, actual: Yes", + 76, ], ]); } diff --git a/tests/PHPStan/Rules/Debug/data/bug-10377.php b/tests/PHPStan/Rules/Debug/data/bug-10377.php new file mode 100644 index 0000000000..b9850376e4 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-10377.php @@ -0,0 +1,37 @@ + $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } +} + +trait RequestParameters +{ + + /** + * @param array $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } + +} diff --git a/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php new file mode 100644 index 0000000000..c7f0ad68f8 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php @@ -0,0 +1,13 @@ + $headers */ +function headers(array $headers): void +{ + if (count($headers) >= 4) { + dumpType(count($headers)); + dumpType($headers); + dumpType(count($headers)); + } +} diff --git a/tests/PHPStan/Rules/Debug/data/debug-scope.php b/tests/PHPStan/Rules/Debug/data/debug-scope.php new file mode 100644 index 0000000000..0e7b8663aa --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/debug-scope.php @@ -0,0 +1,17 @@ + '']); +dumpPhpDocType(["\0" => 'NUL', 'NUL' => "\0"]); +dumpPhpDocType(["\x01" => 'SOH', 'SOH' => "\x01"]); +dumpPhpDocType(["\t" => 'HT', 'HT' => "\t"]); + +// Space +dumpPhpDocType([" " => 'SP', 'SP' => ' ']); +dumpPhpDocType(["foo " => 'ends with SP', " foo" => 'starts with SP', " foo " => 'surrounded by SP', 'foo' => 'no SP']); + +// Punctuation marks +dumpPhpDocType(["foo?" => 'foo?']); +dumpPhpDocType(["shallwedance" => 'yes']); +dumpPhpDocType(["shallwedance?" => 'yes']); +dumpPhpDocType(["Shall we dance" => 'yes']); +dumpPhpDocType(["Shall we dance?" => 'yes']); +dumpPhpDocType(["shall_we_dance" => 'yes']); +dumpPhpDocType(["shall_we_dance?" => 'yes']); +dumpPhpDocType(["shall-we-dance" => 'yes']); +dumpPhpDocType(["shall-we-dance?" => 'yes']); +dumpPhpDocType(['Let\'s go' => "Let's go"]); +dumpPhpDocType(['Foo\\Bar' => 'Foo\\Bar']); +dumpPhpDocType(['3.14' => 3.14]); +dumpPhpDocType([true => true, false => false]); + +/** + * @template T + * @param T $value + * @return T + */ +function id($value) +{ + dumpPhpDocType($value); + + return $value; +} diff --git a/tests/PHPStan/Rules/Debug/data/file-asserts.php b/tests/PHPStan/Rules/Debug/data/file-asserts.php index 5f9fb9cc8d..153c1b0e6c 100644 --- a/tests/PHPStan/Rules/Debug/data/file-asserts.php +++ b/tests/PHPStan/Rules/Debug/data/file-asserts.php @@ -5,6 +5,7 @@ use PHPStan\TrinaryLogic; use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertSuperType; use function PHPStan\Testing\assertVariableCertainty; class Foo @@ -17,6 +18,9 @@ public function doFoo(array $a): void { assertType('array', $a); assertType('array', $a); + + assertSuperType('array', $a); + assertSuperType('array', $a); } /** @@ -24,8 +28,9 @@ public function doFoo(array $a): void */ public function doBar(array $a): void { - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); assertNativeType('array', $a); + assertSuperType('mixed', $a); assertType('false', $a === []); assertType('true', $a !== []); @@ -35,6 +40,12 @@ public function doBar(array $a): void assertNativeType('false', $a === []); assertNativeType('true', $a !== []); + + assertSuperType('bool', $a === []); + assertSuperType('bool', $a !== []); + assertSuperType('mixed', $a === []); + assertSuperType('string', $a === []); + assertSuperType('never', $a === []); } public function doBaz($a): void @@ -46,4 +57,23 @@ public function doBaz($a): void assertVariableCertainty(TrinaryLogic::createMaybe(), $b); } + /** + * @param array{firstName: string, lastName?: string, sub: array{other: string}} $context + */ + public function arrayOffset(array $context) : void + { + assertVariableCertainty(TrinaryLogic::createYes(), $context['firstName']); + assertVariableCertainty(TrinaryLogic::createYes(), $context['sub']); + assertVariableCertainty(TrinaryLogic::createYes(), $context['sub']['other']); + + assertVariableCertainty(TrinaryLogic::createMaybe(), $context['lastName']); + assertVariableCertainty(TrinaryLogic::createMaybe(), $context['nonexistent']['somethingElse']); + + assertVariableCertainty(TrinaryLogic::createNo(), $context['sub']['nonexistent']); + assertVariableCertainty(TrinaryLogic::createNo(), $context['email']); + + // Deliberate error: + assertVariableCertainty(TrinaryLogic::createNo(), $context['firstName']); + } + } diff --git a/tests/PHPStan/Rules/DirectRegistryTest.php b/tests/PHPStan/Rules/DirectRegistryTest.php new file mode 100644 index 0000000000..c1ac9fc109 --- /dev/null +++ b/tests/PHPStan/Rules/DirectRegistryTest.php @@ -0,0 +1,49 @@ +getRules(Node\Expr\FuncCall::class); + $this->assertCount(1, $rules); + $this->assertSame($rule, $rules[0]); + + $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); + } + + public function testGetRulesWithTwoDifferentInstances(): void + { + $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Foo error')->identifier('tests.fooRule')->build(), + ]); + $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Bar error')->identifier('tests.barRule')->build(), + ]); + + $registry = new DirectRegistry([ + $fooRule, + $barRule, + ]); + + $rules = $registry->getRules(Node\Expr\FuncCall::class); + $this->assertCount(2, $rules); + $this->assertSame($fooRule, $rules[0]); + $this->assertSame($barRule, $rules[1]); + + $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); + } + +} diff --git a/tests/PHPStan/Rules/DummyCollector.php b/tests/PHPStan/Rules/DummyCollector.php new file mode 100644 index 0000000000..92f0e162a4 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollector.php @@ -0,0 +1,30 @@ + + */ +class DummyCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->name instanceof Node\Identifier) { + return null; + } + + return $node->name->toString(); + } + +} diff --git a/tests/PHPStan/Rules/DummyCollectorRule.php b/tests/PHPStan/Rules/DummyCollectorRule.php new file mode 100644 index 0000000000..22b66a7809 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollectorRule.php @@ -0,0 +1,50 @@ + + */ +class DummyCollectorRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $data = $node->get(DummyCollector::class); + $methods = []; + foreach ($data as $methodNames) { + foreach ($methodNames as $methodName) { + if (!isset($methods[$methodName])) { + $methods[$methodName] = 0; + } + + $methods[$methodName]++; + } + } + + $parts = []; + foreach ($methods as $methodName => $count) { + $parts[] = sprintf('%d× %s', $count, $methodName); + } + + return [ + RuleErrorBuilder::message(implode(', ', $parts)) + ->file(__DIR__ . '/data/dummy-collector.php') + ->line(5) + ->identifier('tests.dummyCollector') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DummyCollectorRuleTest.php b/tests/PHPStan/Rules/DummyCollectorRuleTest.php new file mode 100644 index 0000000000..175a44d686 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollectorRuleTest.php @@ -0,0 +1,35 @@ + + */ +class DummyCollectorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DummyCollectorRule(); + } + + protected function getCollectors(): array + { + return [ + new DummyCollector(), + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/dummy-collector.php'], [ + [ + '2× doFoo, 2× doBar', + 5, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/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..1028c66778 --- /dev/null +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -0,0 +1,62 @@ + + */ +class EnumCaseAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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, + ), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..8dd7f30a05 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -0,0 +1,80 @@ + + */ +class AbilityToDisableImplicitThrowsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + self::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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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/Bug11900Test.php b/tests/PHPStan/Rules/Exceptions/Bug11900Test.php new file mode 100644 index 0000000000..c8e615b473 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/Bug11900Test.php @@ -0,0 +1,41 @@ + + */ +class Bug11900Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInMethodThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + [], + )), + ); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-11900.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-11900.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/Bug5364Test.php b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php index 88e1e3dcf6..9049c7ad16 100644 --- a/tests/PHPStan/Rules/Exceptions/Bug5364Test.php +++ b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** @@ -10,16 +11,16 @@ class Bug5364Test extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new MissingCheckedExceptionInMethodThrowsRule( new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], - [] - )) + [], + )), ); } diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php new file mode 100644 index 0000000000..421fdb719d --- /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( + self::createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/catch-with-unthrown-exception-stubs.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 55, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/catch-with-unthrown-exception-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 15c4930e07..272c010d97 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -2,8 +2,11 @@ namespace PHPStan\Rules\Exceptions; +use Error; +use InvalidArgumentException; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -11,9 +14,20 @@ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase { + private bool $reportUncheckedExceptionDeadCatch = true; + + /** @var string[] */ + private array $uncheckedExceptionClasses = []; + protected function getRule(): Rule { - return new CatchWithUnthrownExceptionRule(); + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + $this->uncheckedExceptionClasses, + [], + [], + ), $this->reportUncheckedExceptionDeadCatch); } public function testRule(): void @@ -107,6 +121,148 @@ public function testRule(): void 'Dead catch - Exception is never thrown in the try block.', 532, ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 629, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 647, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 741, + ], + [ + 'Dead catch - ArithmeticError is never thrown in the try block.', + 762, + ], + ]); + } + + public function testRuleWithoutReportingUncheckedException(): void + { + $this->reportUncheckedExceptionDeadCatch = false; + $this->uncheckedExceptionClasses = [ + InvalidArgumentException::class, + Error::class, + ]; + + $this->analyse([__DIR__ . '/data/unthrown-exception.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 21, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 38, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 49, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 71, + ], + [ + 'Dead catch - DomainException is never thrown in the try block.', + 117, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 119, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 171, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 180, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 224, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 312, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 344, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 375, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 380, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 398, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 432, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 437, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 485, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 532, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + ]); + } + + public function testMultiCatch(): void + { + $this->analyse([__DIR__ . '/data/unthrown-exception-multi.php'], [ + [ + 'Dead catch - LogicException is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - OverflowException is never thrown in the try block.', + 36, + ], + [ + 'Dead catch - JsonException is never thrown in the try block.', + 58, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 120, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 145, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 156, + ], ]); } @@ -143,12 +299,14 @@ public function testBug4863(): void $this->analyse([__DIR__ . '/data/bug-4863.php'], []); } - public function testBug4814(): void + #[RequiresPhp('>= 8.0')] + public function testBug5866(): void { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3.'); - } + $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.', @@ -157,12 +315,18 @@ public function testBug4814(): void ]); } - public function testThrowExpression(): void + public function testBug9066(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } + $this->analyse([__DIR__ . '/data/bug-9066.php'], [ + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 28, + ], + ]); + } + public function testThrowExpression(): void + { $this->analyse([__DIR__ . '/data/dead-catch-throw-expr.php'], [ [ 'Dead catch - InvalidArgumentException is never thrown in the try block.', @@ -181,4 +345,302 @@ public function testDeadCatch(): void ]); } + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6115(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testUnionTypeError(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9568(): void + { + $this->analyse([__DIR__ . '/data/bug-9568.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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 9875365e55..c56c830bb6 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 = self::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, ); } @@ -50,4 +60,17 @@ public function testBug3690(): void $this->analyse([__DIR__ . '/data/bug-3690.php'], []); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 19, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php index b9a70b2ac9..1ca947f5d6 100644 --- a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php +++ b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php @@ -2,14 +2,18 @@ namespace PHPStan\Rules\Exceptions; +use DomainException; +use InvalidArgumentException; +use LogicException; use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\ScopeFactory; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class DefaultExceptionTypeResolverTest extends PHPStanTestCase { - public function dataIsCheckedException(): array + public static function dataIsCheckedException(): array { return [ [ @@ -17,7 +21,7 @@ public function dataIsCheckedException(): array [], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -27,47 +31,47 @@ public function dataIsCheckedException(): array [], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ [], [ - \InvalidArgumentException::class, + InvalidArgumentException::class, ], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ [], [ - \LogicException::class, + LogicException::class, ], [], [], - \LogicException::class, + LogicException::class, false, ], [ [], [ - \LogicException::class, + LogicException::class, ], [], [], - \DomainException::class, + DomainException::class, false, ], [ [], [ - \DomainException::class, + DomainException::class, ], [], [], - \LogicException::class, + LogicException::class, true, ], [ @@ -77,7 +81,7 @@ public function dataIsCheckedException(): array '#^Exception$#', ], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ @@ -87,7 +91,7 @@ public function dataIsCheckedException(): array '#^InvalidArgumentException#', ], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -95,9 +99,9 @@ public function dataIsCheckedException(): array [], [], [ - \DomainException::class, + DomainException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ @@ -105,9 +109,9 @@ public function dataIsCheckedException(): array [], [], [ - \InvalidArgumentException::class, + InvalidArgumentException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -115,33 +119,31 @@ public function dataIsCheckedException(): array [], [], [ - \LogicException::class, + LogicException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], ]; } /** - * @dataProvider dataIsCheckedException * @param string[] $uncheckedExceptionRegexes * @param string[] $uncheckedExceptionClasses * @param string[] $checkedExceptionRegexes * @param string[] $checkedExceptionClasses - * @param string $className - * @param bool $expectedResult */ + #[DataProvider('dataIsCheckedException')] public function testIsCheckedException( array $uncheckedExceptionRegexes, array $uncheckedExceptionClasses, array $checkedExceptionRegexes, array $checkedExceptionClasses, string $className, - bool $expectedResult + bool $expectedResult, ): void { - $resolver = new DefaultExceptionTypeResolver($this->createReflectionProvider(), $uncheckedExceptionRegexes, $uncheckedExceptionClasses, $checkedExceptionRegexes, $checkedExceptionClasses); + $resolver = new DefaultExceptionTypeResolver(self::createReflectionProvider(), $uncheckedExceptionRegexes, $uncheckedExceptionClasses, $checkedExceptionRegexes, $checkedExceptionClasses); $this->assertSame($expectedResult, $resolver->isCheckedException($className, self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__DIR__)))); } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php index 67b7f7c96a..5c0d007b84 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; /** @@ -15,12 +16,12 @@ protected function getRule(): Rule { return new MissingCheckedExceptionInFunctionThrowsRule( new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], - [\PHPStan\ShouldNotHappenException::class], + [ShouldNotHappenException::class], [], - [] - )) + [], + )), ); } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index 97797eef37..aee538052e 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -3,7 +3,10 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; +use function sprintf; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,18 +18,18 @@ protected function getRule(): Rule { return new MissingCheckedExceptionInMethodThrowsRule( new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], - [\PHPStan\ShouldNotHappenException::class], + [ShouldNotHappenException::class], [], - [] - )) + [], + )), ); } public function testRule(): void { - $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], [ + $errors = [ [ 'Method MissingExceptionMethodThrows\Foo::doBaz() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 23, @@ -39,7 +42,32 @@ public function testRule(): void 'Method MissingExceptionMethodThrows\Foo::doLorem2() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 34, ], - ]); + [ + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateTimeZoneDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateInvalidTimeZoneException' : 'Exception', + ), + 95, + ], + [ + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateIntervalDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateMalformedIntervalStringException' : 'Exception', + ), + 105, + ], + ]; + if (PHP_VERSION_ID >= 80300) { + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 121, + ]; + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 122, + ]; + } + $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php new file mode 100644 index 0000000000..4d46df5def --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php @@ -0,0 +1,52 @@ + + */ +class MissingCheckedExceptionInPropertyHookThrowsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInPropertyHookThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [ShouldNotHappenException::class], + [], + [], + )), + ); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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..55cf2f2628 --- /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 static 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, + [], + ], + ]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataRule')] + public function testRule(int $phpVersion, array $expectedErrors): void + { + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } + + $this->analyse([ + __DIR__ . '/data/noncapturing-catch.php', + __DIR__ . '/data/bug-8663.php', + ], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php new file mode 100644 index 0000000000..a415096826 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php @@ -0,0 +1,71 @@ + + */ +class ThrowExprTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ThrowExprTypeRule(new RuleLevelHelper(self::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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [ + [ + 'Invalid type Exception|null to throw.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php index 2134ca920c..f7143269c3 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -12,15 +13,14 @@ class ThrowExpressionRuleTest extends RuleTestCase { - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; protected function getRule(): Rule { return new ThrowExpressionRule($this->phpVersion); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -40,16 +40,11 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * @param int $phpVersion - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $expectedErrors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->phpVersion = new PhpVersion($phpVersion); $this->analyse([__DIR__ . '/data/throw-expr.php'], $expectedErrors); } diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php index 710e90e77f..af4f7bc372 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use ThrowsVoidFunction\MyException; /** * @extends RuleTestCase @@ -11,24 +13,23 @@ class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase { - /** @var bool */ - private $missingCheckedExceptionInThrows; + private bool $missingCheckedExceptionInThrows; /** @var string[] */ - private $checkedExceptionClasses; + private array $checkedExceptionClasses; protected function getRule(): Rule { return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], - $this->checkedExceptionClasses + $this->checkedExceptionClasses, ), $this->missingCheckedExceptionInThrows); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -48,7 +49,7 @@ public function dataRule(): array ], [ true, - [\ThrowsVoidFunction\MyException::class], + [MyException::class], [], ], [ @@ -73,7 +74,7 @@ public function dataRule(): array ], [ false, - [\ThrowsVoidFunction\MyException::class], + [MyException::class], [ [ 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', @@ -85,11 +86,10 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * @param bool $missingCheckedExceptionInThrows * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php index 15ba07e2ce..196d99a84c 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -4,6 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use ThrowsVoidMethod\MyException; +use UnhandledMatchError; /** * @extends RuleTestCase @@ -11,24 +15,23 @@ class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase { - /** @var bool */ - private $missingCheckedExceptionInThrows; + private bool $missingCheckedExceptionInThrows; /** @var string[] */ - private $checkedExceptionClasses; + private array $checkedExceptionClasses; protected function getRule(): Rule { return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], - $this->checkedExceptionClasses + $this->checkedExceptionClasses, ), $this->missingCheckedExceptionInThrows); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -48,7 +51,7 @@ public function dataRule(): array ], [ true, - [\ThrowsVoidMethod\MyException::class], + [MyException::class], [], ], [ @@ -73,7 +76,7 @@ public function dataRule(): array ], [ false, - [\ThrowsVoidMethod\MyException::class], + [MyException::class], [ [ 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', @@ -85,11 +88,10 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * @param bool $missingCheckedExceptionInThrows * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; @@ -97,4 +99,12 @@ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedEx $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); } + #[RequiresPhp('>= 8.0')] + public function testBug6910(): void + { + $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..e7db139575 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php @@ -0,0 +1,117 @@ + + */ +class ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidPropertyHookWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public static 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, + ], + ], + ], + ]; + } + + /** + * @param string[] $checkedExceptionClasses + * @param list $errors + */ + #[RequiresPhp('>= 8.4')] + #[DataProvider('dataRule')] + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-property-hook.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php index 82871e6145..58aea82f3a 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php @@ -13,12 +13,16 @@ class TooWideFunctionThrowTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck()); + return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck(true)); } public function testRule(): void { $this->analyse([__DIR__ . '/data/too-wide-throws-function.php'], [ + [ + 'Function TooWideThrowsFunction\doFoo3() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 20, + ], [ 'Function TooWideThrowsFunction\doFoo4() has DomainException in PHPDoc @throws tag but it\'s not thrown.', 26, diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index eddbd31bf6..85cff48d9b 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -12,14 +14,20 @@ class TooWideMethodThrowTypeRuleTest extends RuleTestCase { + private bool $implicitThrows = true; + protected function getRule(): Rule { - return new TooWideMethodThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck()); + return new TooWideMethodThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows)); } public function testRule(): void { $this->analyse([__DIR__ . '/data/too-wide-throws-method.php'], [ + [ + 'Method TooWideThrowsMethod\Foo::doFoo3() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 23, + ], [ 'Method TooWideThrowsMethod\Foo::doFoo4() has DomainException in PHPDoc @throws tag but it\'s not thrown.', 29, @@ -40,7 +48,59 @@ public function testRule(): void 'Method TooWideThrowsMethod\ParentClass::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.', 77, ], + [ + 'Method TooWideThrowsMethod\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 167, + ], ]); } + public function testBug6233(): void + { + $this->analyse([__DIR__ . '/data/bug-6233.php'], []); + } + + public function testImmediatelyCalledArrowFunction(): void + { + $this->analyse([__DIR__ . '/data/immediately-called-arrow-function.php'], [ + [ + 'Method ImmediatelyCalledArrowFunction\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testFirstClassCallable(): void + { + $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, + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataRuleLookOnlyForExplicitThrowPoints')] + 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..2db813da37 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php @@ -0,0 +1,52 @@ + + */ +class TooWidePropertyHookThrowTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck(true)); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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-11900.neon b/tests/PHPStan/Rules/Exceptions/bug-11900.neon new file mode 100644 index 0000000000..93a3c1512a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/bug-11900.neon @@ -0,0 +1,6 @@ +parameters: + exceptions: + implicitThrows: false + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true diff --git a/tests/PHPStan/Rules/Exceptions/bug-5364.neon b/tests/PHPStan/Rules/Exceptions/bug-5364.neon index 9fa19a4a89..93a3c1512a 100644 --- a/tests/PHPStan/Rules/Exceptions/bug-5364.neon +++ b/tests/PHPStan/Rules/Exceptions/bug-5364.neon @@ -1,6 +1,6 @@ parameters: - implicitThrows: false 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-11900.php b/tests/PHPStan/Rules/Exceptions/data/bug-11900.php new file mode 100644 index 0000000000..f470ca5dfd --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-11900.php @@ -0,0 +1,50 @@ += 8.4 + +namespace Bug11900; + +use Exception; +use Throwable; + +abstract class ADataException extends Exception +{ + /** + * @return void + * @throws static + */ + public function throw1(): void + { + throw $this; + } + + /** + * @return void + * @throws static + */ + public static function throw2(): void + { + throw new static(); + } +} + +final class TestDataException extends ADataException +{ +} + +class TestPhpStan +{ + /** + * @throws TestDataException + */ + public function validate(TestDataException $e): void + { + $e->throw1(); + } + + /** + * @throws TestDataException + */ + public function validate2(): void + { + TestDataException::throw2(); + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4852.php b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php new file mode 100644 index 0000000000..dfc01e41c8 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php @@ -0,0 +1,88 @@ += 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/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 index fa699f6bd7..443b716b94 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php @@ -56,3 +56,10 @@ function doBar3(): void { throw new \LogicException(); // error } + +function bug13288(array $a): void +{ + array_push($a, function() { + throw new \LogicException(); // ok, as array_push() will not invoke the function + }); +} diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 66c280bf79..21cfdf1072 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -57,4 +57,69 @@ public function doDolor(): void } } + public function doSit(): void + { + try { + $this->throwsInterface(); + } catch (\Throwable $e) { + + } + } + + public function doSit2(): void + { + try { + $this->throwsInterface(); + } catch (\InvalidArgumentException $e) { + + } catch (\Throwable $e) { + + } + } + + /** + * @throws \ExtendsThrowable\ExtendsThrowable + */ + private function throwsInterface(): void + { + + } + + public function dateTimeZoneDoesNotThrow(): void + { + new \DateTimeZone('UTC'); + } + + public function dateTimeZoneDoesThrows(string $tz): void + { + new \DateTimeZone($tz); + } + + public function dateTimeZoneDoesNotThrowCaseInsensitive(): void + { + new \DaTetImezOnE('UTC'); + } + + public function dateIntervalDoesThrows(string $i): void + { + new \DateInterval($i); + } + + public function dateIntervalDoeNotThrow(): void + { + new \DateInterval('P7D'); + } + + public function dateTimeModifyDoeNotThrow(\DateTime $dt, \DateTimeImmutable $dti): void + { + $dt->modify('+1 day'); + $dti->modify('+1 day'); + } + + public function dateTimeModifyDoesThrows(\DateTime $dt, \DateTimeImmutable $dti, string $m): void + { + $dt->modify($m); + $dti->modify($m); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php new file mode 100644 index 0000000000..773f849d74 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php @@ -0,0 +1,46 @@ += 8.4 + +namespace MissingExceptionPropertyHookThrows; + +class Foo +{ + + public int $i { + /** @throws \InvalidArgumentException */ + get { + throw new \InvalidArgumentException(); // ok + } + } + + public int $j { + /** @throws \LogicException */ + set { + throw new \InvalidArgumentException(); // ok + } + } + + public int $k { + /** @throws \RuntimeException */ + get { + throw new \InvalidArgumentException(); // error + } + } + + public int $l { + /** @throws \RuntimeException */ + set { + throw new \InvalidArgumentException(); // error + } + } + + public int $m { + get { + throw new \InvalidArgumentException(); // error + } + } + + public int $n { + get => throw new \InvalidArgumentException(); // error + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php new file mode 100644 index 0000000000..a9e8a482cb --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php @@ -0,0 +1,17 @@ += 8.0 + +namespace NoncapturingCatch; + +class HelloWorld +{ + + public function hello(): void + { + try { + throw new \Exception('Hello'); + } catch (\Exception) { + echo 'Hi!'; + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/throw-class-exists.php b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php similarity index 83% rename from tests/PHPStan/Rules/Variables/data/throw-class-exists.php rename to tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php index f819307398..39c9dd13dc 100644 --- a/tests/PHPStan/Rules/Variables/data/throw-class-exists.php +++ b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php @@ -1,6 +1,6 @@ = 8.0 + +namespace 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-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php new file mode 100644 index 0000000000..08e3f10940 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php @@ -0,0 +1,29 @@ += 8.4 + +namespace ThrowsVoidPropertyHook; + +class MyException extends \Exception +{ + +} + +class Foo +{ + + public int $i { + /** + * @throws void + */ + get { + throw new MyException(); + } + } + + public int $j { + /** + * @throws void + */ + get => throw new MyException(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php new file mode 100644 index 0000000000..834df2b1f0 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php @@ -0,0 +1,22 @@ +doBar(); + } + + public function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php index f4bae0e0ae..d537543139 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php @@ -17,7 +17,7 @@ function doFoo2(): void // ok } /** @throws \InvalidArgumentException */ -function doFoo3(): void // ok +function doFoo3(): void // new LogicException cannot be InvalidArgumentException { throw new \LogicException(); } @@ -64,3 +64,9 @@ function doFoo9(): void // error - DomainException unused { } + +/** @throws \InvalidArgumentException */ +function doFoo10(\LogicException $e): void // ok +{ + throw $e; +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php index 819d89c87b..5e28b2186e 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php @@ -20,7 +20,7 @@ public function doFoo2(): void // ok } /** @throws \InvalidArgumentException */ - public function doFoo3(): void // ok + public function doFoo3(): void // // new LogicException cannot be InvalidArgumentException { throw new \LogicException(); } @@ -147,3 +147,49 @@ public function doBaz(): void } } + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(function () { + throw new \InvalidArgumentException(); + }, $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = function () { + throw new \InvalidArgumentException(); + }; + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (function () { + throw new \InvalidArgumentException(); + })(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php new file mode 100644 index 0000000000..6de7bc4073 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php @@ -0,0 +1,95 @@ += 8.4 + +namespace TooWideThrowsPropertyHook; + +use DomainException; + +class Foo +{ + + public int $a { + /** @throws \InvalidArgumentException */ + get { + throw new \InvalidArgumentException(); + } + } + + public int $b { + /** @throws \LogicException */ + get { + throw new \InvalidArgumentException(); + } + } + + public int $c { + /** @throws \InvalidArgumentException */ + get { + throw new \LogicException(); // new LogicException cannot be InvalidArgumentException + } + } + + public int $d { + /** @throws \InvalidArgumentException|\DomainException */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + public int $e { + /** @throws void */ + get { // ok - picked up by different rule + throw new \InvalidArgumentException(); + } + } + + public int $f { + /** @throws \InvalidArgumentException|\DomainException */ + get { + if (rand(0, 1)) { + throw new \InvalidArgumentException(); + } + + throw new DomainException(); + } + } + + public int $g { + /** @throws \DomainException */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + public int $h { + /** + * @throws \InvalidArgumentException + * @throws \DomainException + */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + + public int $j { + /** @throws \DomainException */ + get { // error - DomainException unused + + } + } + + public int $k { + /** @throws \DomainException */ + get => 11; // error - DomainException unused + } + + public int $l { + /** @throws \InvalidArgumentException */ + get { + throw $this->logicException; + } + } + + public \LogicException $logicException; + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/union-type-error.php b/tests/PHPStan/Rules/Exceptions/data/union-type-error.php new file mode 100644 index 0000000000..cad8c5348c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/union-type-error.php @@ -0,0 +1,29 @@ += 8.0 + +declare(strict_types = 1); + +namespace UnionTypeError; + +final class Foo { + public string|int $stringOrInt; + public string|array $stringOrArray; + + public function bar() { + try { + $this->stringOrInt = ""; + } catch (\TypeError $e) {} + + try { + $this->stringOrInt = true; + } catch (\TypeError $e) {} + + try { + $this->stringOrArray = []; + } catch (\TypeError $e) {} + + try { + $this->stringOrInt = $this->stringOrArray; + } catch (\TypeError $e) {} + } +} + diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php new file mode 100644 index 0000000000..c0893a8ba1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php @@ -0,0 +1,187 @@ +throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException $t) { + + } + } + + public function doBaz() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException | \OverflowException $t) { // overflow not thrown + + } + } + + public function doBag() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException $t) { + + } catch (\LogicException | \JsonException $t) { + + } + } + + public function doZag() + { + try { + throw new \RangeException(); + + } catch (\RuntimeException | \JsonException $t) { // json not thrown + + } + } + + public function doBal() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException | \JsonException $t) { + + } catch (\InvalidArgumentException $t) { + + } + } + + public function doBap() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\InvalidArgumentException $t) { + + } catch (\RuntimeException | \JsonException $t) { + + } + } + + public function doZaz() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\LogicException $e) { + + } + } + + public function doZab() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\InvalidArgumentException | \LogicException $e) { + + } + } + + public function someThrowableTest(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest2(): void + { + try { + $this->throwIae(); + } catch (\RuntimeException | \Throwable $e) { + // IAE is not runtime, dead + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest3(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\Throwable $e) { + + } + } + + + public function someThrowableTest4(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest5(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest6(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + // catch can be simplified, this is not reported + } + } + + /** + * @throws \RangeException + * @throws \LogicException + * @throws \JsonException + */ + private function throwLogicRangeJsonExceptions(): void + { + + } + + /** @throws \InvalidArgumentException */ + public function throwIae(): void + { + + } + + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php new file mode 100644 index 0000000000..6d47003630 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php @@ -0,0 +1,120 @@ += 8.4 + +namespace UnthrownExceptionPropertyHooksImplicitThrowsDisabled; + +class MyCustomException extends \Exception +{ + +} + +class SomeException extends \Exception +{ + +} + +class Foo +{ + public int $i; + + public function doFoo(): void + { + try { + echo $this->i; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + public int $k { + get { + return 1; + } + } + + public function doBaz(): void + { + try { + echo $this->k; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + private int $l { + get { + return $this->l; + } + } + + public function doLorem(): void + { + try { + echo $this->l; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + final public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +final class FinalFoo +{ + + public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + +} + +class ThrowsVoid +{ + + public int $m { + /** @throws void */ + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php new file mode 100644 index 0000000000..547a47eece --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php @@ -0,0 +1,200 @@ += 8.4 + +namespace UnthrownExceptionPropertyHooks; + +class MyCustomException extends \Exception +{ + +} + +class SomeException extends \Exception +{ + +} + +class Foo +{ + + public int $i { + /** @throws MyCustomException */ + get { + if (rand(0, 1)) { + throw new MyCustomException(); + } + + try { + return $this->i; + } catch (MyCustomException) { // unthrown - @throws does not apply to direct access in the hook + + } + } + } + + public function doFoo(): void + { + try { + $a = $this->i; + } catch (MyCustomException) { + + } catch (SomeException) { // unthrown + + } + } + + public int $j { + /** @throws MyCustomException */ + set { + if (rand(0, 1)) { + throw new MyCustomException(); + } + + try { + $this->j = $value; + } catch (MyCustomException) { // unthrown - @throws does not apply to direct access in the hook + + } + } + } + + public function doBar(int $v): void + { + try { + $this->j = $v; + } catch (MyCustomException) { + + } catch (SomeException) { // unthrown + + } + } + + public int $k { + get { + return 1; + } + } + + public function doBaz(): void + { + try { + echo $this->k; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->k = 1; + } catch (MyCustomException) { // can be thrown - subclass might introduce a set hook + + } + } + + private int $l { + get { + return $this->l; + } + } + + public function doLorem(): void + { + try { + echo $this->l; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->l = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + + final public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +final class FinalFoo +{ + + public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +class ThrowsVoid +{ + + public int $m { + /** @throws void */ + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown + + } + } + +} + +class Dynamic +{ + + public function doFoo(object $o, string $s): void + { + try { + echo $o->$s; + } catch (MyCustomException) { // implicit throw point + + } + + try { + $o->$s = 1; + } catch (MyCustomException) { // implicit throw point + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 78236201ac..0327fcb086 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -544,3 +544,249 @@ public function doBar(string $s) } } + +class TestCaseInsensitiveClassNames +{ + + public function doFoo(): void + { + try { + new \SimpleXmlElement(''); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \SimpleXmlElement('foo'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \SimpleXmlElement($string); + } catch (\Exception $e) { + + } + } + +} + +/** @throws void */ +function acceptCallable(callable $cb): void +{ + +} + +/** + * @throws void + * @param-later-invoked-callable $cb + */ +function acceptCallableAndCallLater(callable $cb): void +{ + +} + +class CallCallable +{ + + /** + * @throws void + */ + public function doFoo(callable $cb): void + { + try { + $cb(); + } catch (\Exception $e) { + + } + } + + public function passCallableToFunction(): void + { + try { + // immediately called by default + acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function passCallableToFunction2(): void + { + try { + // later called thanks to @param-later-invoked-callable + acceptCallableAndCallLater(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** @throws void */ + public function acceptCallable(callable $cb): void + { + + } + + public function passCallableToMethod(): void + { + try { + // later called by default + $this->acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws void + * @param-immediately-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable extends CallCallable +{ + + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable2 extends CallCallable +{ + + /** + * @param callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable3 extends CallCallable +{ + + /** + * @param callable $cb + * @param-later-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // later called thanks to @param-later-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class TestIntdivWithRange +{ + /** + * @param int $int + * @param int $negativeInt + * @param int<1, max> $positiveInt + */ + public function doFoo(int $int, int $negativeInt, int $positiveInt): void + { + try { + intdiv($int, $positiveInt); + intdiv($positiveInt, $negativeInt); + intdiv($negativeInt, $positiveInt); + intdiv($positiveInt, $positiveInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($positiveInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, '-1,5'); + } catch (\ArithmeticError $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php new file mode 100644 index 0000000000..96be9603a8 --- /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( + self::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..1f3e273b16 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -0,0 +1,92 @@ + + */ +class ArrayValuesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrayValuesRule( + self::createReflectionProvider(), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + ); + } + + public function testFile(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ + [ + 'Parameter #1 $array (array{0, 1, 3}) of array_values is already a list, call has no effect.', + 8, + ], + [ + 'Parameter #1 $array (array{1, 3}) of array_values is already a list, call has no effect.', + 9, + ], + [ + 'Parameter #1 $array (array{\'test\'}) of array_values is already a list, call has no effect.', + 10, + ], + [ + 'Parameter #1 $array (array{\'\', \'test\'}) of array_values is already a list, call has no effect.', + 12, + ], + [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 14, + $tipText, + ], + [ + 'Parameter #1 $array (array{0}) of array_values is already a list, call has no effect.', + 17, + ], + [ + 'Parameter #1 $array (array{null, null}) of array_values is already a list, call has no effect.', + 19, + ], + [ + 'Parameter #1 $array (array{null, 0}) of array_values is already a list, call has no effect.', + 20, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 21, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 25, + $tipText, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $expectedErrors[] = [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 28, + $tipText, + ]; + } else { + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 27, + ]; + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 28, + ]; + } + + $this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 64fcae7e6a..64ce4730fe 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class ArrowFunctionAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ArrowFunctionAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/arrow-function-attributes.php'], [ [ 'Attribute class ArrowFunctionAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php index 22cca6e10e..d046acb657 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/arrow-function-nullsafe-by-ref.php'], [ [ 'Nullsafe cannot be returned by reference.', diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index a3a584919b..99d577361e 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ArrowFunctionReturnTypeRuleTest extends RuleTestCase { @@ -16,19 +17,19 @@ class ArrowFunctionReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { return new ArrowFunctionReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), 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,16 +39,40 @@ public function testRule(): void 'Anonymous function should return int but returns string.', 14, ], + ]); } - public function testBug3261(): void + #[RequiresPhp('>= 8.1')] + public function testRuleNever(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } + $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'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug8179(): void + { + $this->analyse([__DIR__ . '/data/bug-8179.php'], []); + } + + public function testBugSpaceship(): void + { + $this->analyse([__DIR__ . '/data/bug-spaceship.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + $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 221ba5c90b..c82080b9ed 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,46 +2,47 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallCallablesRuleTest extends \PHPStan\Testing\RuleTestCase +class CallCallablesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, false, true); return new CallCallablesRule( new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, true, true, true, - true ), $ruleLevelHelper, - true + true, ); } public function testRule(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped(); - } - $this->analyse([__DIR__ . '/data/callables.php'], [ + $errors = [ [ 'Trying to invoke string but it might not be a callable.', 17, @@ -55,11 +56,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, ], [ @@ -104,46 +105,64 @@ public function testRule(): void 106, ], [ - 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 'Trying to invoke CallCallables\Baz but it\'s not a callable.', 113, ], [ - 'Trying to invoke 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.', - 163, + '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.', - 167, + '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.', - 179, + '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); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to closure.', @@ -168,14 +187,14 @@ public function testNamedArguments(): void ]); } - public function dataBug3566(): array + public static function dataBug3566(): array { return [ [ true, [ [ - 'Parameter #1 $ of closure expects int, TMemberType given.', + 'Parameter #1 of closure expects int, TMemberType given.', 29, ], ], @@ -188,14 +207,119 @@ public function dataBug3566(): array } /** - * @dataProvider dataBug3566 - * @param bool $checkExplicitMixed - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataBug3566')] public function testBug3566(bool $checkExplicitMixed, array $errors): void { $this->checkExplicitMixed = $checkExplicitMixed; $this->analyse([__DIR__ . '/data/bug-3566.php'], $errors); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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'], [ + [ + 'Unable to resolve the template type T in call to closure', + 14, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Unable to resolve the template type T in call to closure', + 17, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + ]); + } + + public function testBug6701(): void + { + $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 testBug3616(): void + { + $this->analyse([__DIR__ . '/data/bug-3616.php'], []); + } + + public function testBug10814(): void + { + $this->analyse([__DIR__ . '/data/bug-10814.php'], [ + [ + 'Parameter #1 of closure expects DateTime, DateTimeImmutable given.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 89544277ab..f485a7adee 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,28 +2,34 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function sprintf; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToFunctionParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class CallToFunctionParametersRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $broker = self::createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), 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), ); } @@ -35,6 +41,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'], [ [ @@ -50,7 +60,7 @@ public function testCallToFunctionWithIncorrectParameters(): void 14, ], [ - 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.', + $setErrorHandlerError, 16, ], ]); @@ -157,7 +167,7 @@ public function testCallToWeirdFunctions(): void 11, ], [ - 'Function fputcsv invoked with 1 parameter, 2-5 required.', + sprintf('Function fputcsv invoked with 1 parameter, 2-%d required.', PHP_VERSION_ID >= 80100 ? 6 : 5), 12, ], [ @@ -264,14 +274,8 @@ public function testCallToWeirdFunctions(): void $this->analyse([__DIR__ . '/data/call-to-weird-functions.php'], $errors); } - /** - * @requires PHP 7.1.1 - */ public function testUnpackOnAfter711(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70101) { - $this->markTestSkipped('This test requires PHP >= 7.1.1'); - } $this->analyse([__DIR__ . '/data/unpack.php'], [ [ 'Function unpack invoked with 0 parameters, 2-3 required.', @@ -296,15 +300,15 @@ public function testPassingNonVariableToParameterPassedByReference(): void 'Parameter #1 $array of function reset expects array|object, null given.', 39, ], + [ + 'Parameter #1 $s of function PassedByReference\bar expects string, int given.', + 48, + ], ]); } public function testImplodeOnPhp74(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -315,13 +319,14 @@ 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.', + 'Parameter #1 $separator of function implode expects string, array given.', + 8, + ], + [ + 'Parameter #2 $array of function implode expects array, string given.', 8, ], ]; @@ -332,19 +337,18 @@ public function testImplodeOnPhp74(): void public function testImplodeOnLessThanPhp74(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test skipped on 7.4.'); - } - - $errors = []; if (PHP_VERSION_ID >= 80000) { $errors = [ [ - 'Parameter #2 $array of function implode expects array|null, string given.', + 'Parameter #1 $separator of function implode expects string, array given.', + 8, + ], + [ + 'Parameter #2 $array of function implode expects array, string given.', 8, ], ]; - } elseif (PHP_VERSION_ID >= 70400) { + } else { $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -360,6 +364,21 @@ public function testImplodeOnLessThanPhp74(): void $this->analyse([__DIR__ . '/data/implode-74.php'], $errors); } + #[RequiresPhp('>= 8.0')] + public function testImplodeNamedParameters(): void + { + $this->analyse([__DIR__ . '/data/implode-named-parameters.php'], [ + [ + 'Missing parameter $separator (string) in call to function implode.', + 6, + ], + [ + 'Missing parameter $separator (string) in call to function join.', + 7, + ], + ]); + } + public function testVariableIsNotNullAfterSeriesOfConditions(): void { require_once __DIR__ . '/data/variable-is-not-null-after-conditions.php'; @@ -427,13 +446,9 @@ public function testFputCsv(): void ]); } - + #[RequiresPhp('>= 8.0')] 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 ]); @@ -481,12 +496,9 @@ public function testGenericFunction(): void ]); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = [ [ 'Missing parameter $j (int) in call to function FunctionNamedArguments\foo.', @@ -501,17 +513,22 @@ public function testNamedArguments(): void 14, ], ]; - if (PHP_VERSION_ID < 80000) { - $errors[] = [ - 'Missing parameter $arr1 (array) in call to function array_merge.', - 14, - ]; - } require_once __DIR__ . '/data/named-arguments-define.php'; $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); } + #[RequiresPhp('>= 8.1')] + public function testNamedArgumentsAfterUnpacking(): void + { + $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'], []); @@ -544,17 +561,19 @@ 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'], []); } + #[RequiresPhp('>= 8.0')] 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'], []); } @@ -562,37 +581,38 @@ public function testArrayReduceCallback(): void { $this->analyse([__DIR__ . '/data/array_reduce.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 13, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 22, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } public function testArrayReduceArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/array_reduce_arrow.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 11, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 18, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } @@ -601,30 +621,107 @@ public function testArrayWalkCallback(): void { $this->analyse([__DIR__ . '/data/array_walk.php'], [ [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, mixed): mixed, Closure(stdClass, float): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(stdClass, float): \'\' given.', 6, ], [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, string): mixed, Closure(int, string, int): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.', 14, ], + [ + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', + 23, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], ]); } public function testArrayWalkArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/array_walk_arrow.php'], [ [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, mixed): mixed, Closure(stdClass, float): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(stdClass, float): \'\' given.', 6, ], [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, string): mixed, Closure(int, string, int): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.', 12, ], + [ + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', + 19, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], + ]); + } + + public function testArrayUdiffCallback(): void + { + $this->analyse([__DIR__ . '/data/array_udiff.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(string, string): string given.', + 6, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string) given.', + 14, + ], + [ + 'Parameter #1 $arr1 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', + 20, + ], + [ + 'Parameter #2 $arr2 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', + 21, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(string, string): int, Closure(string, int): non-empty-string given.', + 22, + ], + ]); + } + + 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, + ], ]); } @@ -632,7 +729,7 @@ public function testUasortCallback(): void { $this->analyse([__DIR__ . '/data/uasort.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -640,13 +737,9 @@ public function testUasortCallback(): void public function testUasortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/uasort_arrow.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -656,7 +749,7 @@ public function testUsortCallback(): void { $this->analyse([__DIR__ . '/data/usort.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -664,13 +757,9 @@ public function testUsortCallback(): void public function testUsortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/usort_arrow.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -680,7 +769,7 @@ public function testUksortCallback(): void { $this->analyse([__DIR__ . '/data/uksort.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -692,13 +781,9 @@ public function testUksortCallback(): void public function testUksortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/uksort_arrow.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -721,10 +806,6 @@ public function testVaryingAcceptor(): void public function testBug3660(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-3660.php'], [ [ 'Parameter #1 $string of function strlen expects string, int given.', @@ -737,12 +818,9 @@ public function testBug3660(): void ]); } + #[RequiresPhp('>= 8.0')] 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.', @@ -761,14 +839,11 @@ public function testExplode(): void public function testProcOpen(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/proc_open.php'], [ [ - 'Parameter #1 $command of function proc_open expects array|string, array given.', + "Parameter #1 \$command of function proc_open expects list|string, array{something: 'bogus', in: 'here'} given.", 6, + "Type #1 from the union: array{something: 'bogus', in: 'here'} is not a list.", ], ]); } @@ -779,7 +854,7 @@ public function testBug5609(): void $this->analyse([__DIR__ . '/data/bug-5609.php'], []); } - public function dataArrayMapMultiple(): array + public static function dataArrayMapMultiple(): array { return [ [true], @@ -787,22 +862,19 @@ public function dataArrayMapMultiple(): array ]; } - /** - * @dataProvider dataArrayMapMultiple - * @param bool $checkExplicitMixed - */ + #[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, Closure(int, int): void given.', + '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 + public static function dataArrayFilterCallback(): array { return [ [true], @@ -810,41 +882,141 @@ public function dataArrayFilterCallback(): array ]; } - /** - * @dataProvider dataArrayFilterCallback - * @param bool $checkExplicitMixed - */ + #[DataProvider('dataArrayFilterCallback')] public function testArrayFilterCallback(bool $checkExplicitMixed): void { $this->checkExplicitMixed = $checkExplicitMixed; $errors = [ [ - 'Parameter #2 $callback of function array_filter expects callable(int): mixed, Closure(string): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(int): bool)|null, Closure(string): true given.', 17, ], ]; if ($checkExplicitMixed) { $errors[] = [ - 'Parameter #2 $callback of function array_filter expects callable(mixed): mixed, Closure(int): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(mixed): bool)|null, Closure(int): true given.', 20, + 'Type #1 from the union: Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ]; } $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } - public function testBug5356(): void + #[RequiresPhp('>= 8.4')] + public function testArrayAllCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testArrayAnyCallback(): void + { + $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, Closure(array): \'a\' given.', + '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, Closure(array): \'a\' given.', + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', 21, ], ]); @@ -854,7 +1026,7 @@ public function testBug1954(): void { $this->analyse([__DIR__ . '/data/bug-1954.php'], [ [ - 'Parameter #1 $callback of function array_map expects callable(1|stdClass): mixed, Closure(string): string given.', + 'Parameter #1 $callback of function array_map expects (callable(1|stdClass): mixed)|null, Closure(string): string given.', 7, ], ]); @@ -864,7 +1036,7 @@ public function testBug2782(): void { $this->analyse([__DIR__ . '/data/bug-2782.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): -1|1 given.', + 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): (-1|1) given.', 13, ], ]); @@ -876,4 +1048,1287 @@ public function testBug5661(): void $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.", + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7211(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7138(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9283(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testJsonValidate(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9699(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9018(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9399(): void + { + $this->analyse([__DIR__ . '/data/bug-9399.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9559(): void + { + $this->analyse([__DIR__ . '/data/bug-9559.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9923(): void + { + $this->analyse([__DIR__ . '/data/bug-9923.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9823(): void + { + $this->analyse([__DIR__ . '/data/bug-9823.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedParametersForMultiVariantFunctions(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testCallToArrayFilterWithNullCallback(): void + { + $this->analyse([__DIR__ . '/data/array_filter_null_callback.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10171(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7283(): void + { + $this->analyse([__DIR__ . '/data/bug-7283.php'], []); + } + + public function testBug9697(): void + { + $this->analyse([__DIR__ . '/data/bug-9697.php'], []); + } + + public function testDiscussion10454(): void + { + $this->analyse([__DIR__ . '/data/discussion-10454.php'], [ + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 13, + ], + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 23, + ], + ]); + } + + public function 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10297(): void + { + $this->analyse([__DIR__ . '/data/bug-10297.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10974(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testNoNamedArguments(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9224(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8046(): void + { + $this->analyse([__DIR__ . '/data/bug-8046.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11942(): void + { + $this->analyse([__DIR__ . '/data/bug-11942.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11418(): void + { + $this->analyse([__DIR__ . '/data/bug-11418.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9167(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12847(): void + { + $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, + ], + ]); + } + + public function testBug12954(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12954.php'], []); + } + + public function testBug8922(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + 'Parameter #1 $encoding_list of function mb_detect_order expects non-empty-list|non-falsy-string, null given.', + 15, + '• Type #1 from the union: null is not a list. +• Type #1 from the union: null is empty.', + ]; + } + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8922.php'], $errors); + } + + public function testBug10020(): void + { + $this->analyse([__DIR__ . '/data/bug-10020.php'], []); + } + + public function testBug8506(): void + { + $this->analyse([__DIR__ . '/data/bug-8506.php'], []); + } + + public function testBug7772(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + 'Parameter #2 $path of function session_set_cookie_params expects string, string|null given.', + 12, + ]; + $errors[] = [ + 'Parameter #3 $domain of function session_set_cookie_params expects string, string|null given.', + 12, + ]; + $errors[] = [ + 'Parameter #4 $secure of function session_set_cookie_params expects bool, bool|null given.', + 12, + ]; + $errors[] = [ + 'Parameter #5 $httponly of function session_set_cookie_params expects bool, bool|null given.', + 12, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-7772.php'], $errors); + } + + public function testBug13065(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + 'Parameter #1 $varname of function getenv expects string, null given.', + 10, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-13065.php'], $errors); + } + + public function testBug3506(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3506.php'], []); + } + + public function testBug5760(): void + { + if (PHP_VERSION_ID < 80000) { + $param1Name = '$glue'; + $param2Name = '$pieces'; + } else { + $param1Name = '$separator'; + $param2Name = '$array'; + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5760.php'], [ + [ + sprintf('Parameter #2 %s of function join expects array, list|null given.', $param2Name), + 10, + ], + [ + sprintf('Parameter #1 %s of function join expects array, list|null given.', $param1Name), + 11, + ], + [ + sprintf('Parameter #2 %s of function implode expects array, list|null given.', $param2Name), + 13, + ], + [ + sprintf('Parameter #1 %s of function implode expects array, list|null given.', $param1Name), + 14, + ], + [ + sprintf('Parameter #2 %s of function join expects array, array|string given.', $param2Name), + 22, + ], + [ + sprintf('Parameter #1 %s of function join expects array, array|string given.', $param1Name), + 23, + ], + [ + sprintf('Parameter #2 %s of function implode expects array, array|string given.', $param2Name), + 25, + ], + [ + sprintf('Parameter #1 %s of function implode expects array, array|string given.', $param1Name), + 26, + ], + ]); + } + + public function testBug9970(): void + { + $this->analyse([__DIR__ . '/data/bug-9970.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12317(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12317.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.', + 28, + ], + [ + 'Parameter $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.', + 29, + ], + ]); + } + + public function testBug13197(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13197.php'], []); + } + + public function testBug13556(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13556.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..8c1abccdad --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php @@ -0,0 +1,51 @@ + + */ +class CallToFunctionStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithNoDiscardRule(self::createReflectionProvider()); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-call-statement-result-discarded.php'], [ + [ + 'Call to function FunctionCallStatementResultDiscarded\withSideEffects() on a separate line discards return value.', + 11, + ], + [ + 'Call to function FunctionCallStatementResultDiscarded\differentCase() on a separate line discards return value.', + 25, + ], + [ + 'Call to callable \'FunctionCallStateme…\' on a separate line discards return value.', + 30, + ], + [ + 'Call to callable Closure(): array on a separate line discards return value.', + 35, + ], + [ + 'Call to callable Closure(): 1 on a separate line discards return value.', + 40, + ], + [ + 'Call to callable Closure(): 1 on a separate line discards return value.', + 45, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index f47f1f4461..6d2fe2bbba 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -4,16 +4,17 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class CallToFunctionStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new CallToFunctionStatementWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToFunctionStatementWithoutSideEffectsRule(self::createReflectionProvider()); } public function testRule(): void @@ -23,6 +24,33 @@ public function testRule(): void 'Call to function sprintf() on a separate line has no effect.', 13, ], + [ + 'Call to function var_export() on a separate line has no effect.', + 24, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 26, + ], + ]); + + if (PHP_VERSION_ID < 80000) { + return; + } + + $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-8.0.php'], [ + [ + 'Call to function var_export() on a separate line has no effect.', + 19, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 20, + ], + [ + 'Call to function highlight_string() on a separate line has no effect.', + 21, + ], ]); } @@ -43,16 +71,51 @@ public function testPhpDoc(): void 10, ], [ - 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure4() on a separate line has no effect.', 11, ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure5() on a separate line has no effect.', + 12, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 13, + ], ]); } + public function testBug12224(): void + { + $this->analyse([__DIR__ . '/data/bug-12224.php'], []); + } + 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 ef1abc2ea3..00932fa18f 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 PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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(self::createReflectionProvider(), true, true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool { - return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true); + return true; } public function testEmptyFile(): void @@ -31,7 +40,6 @@ public function testCallToNonexistentFunction(): void 'Function foobarNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -43,7 +51,6 @@ public function testCallToNonexistentNestedFunction(): void 'Function barNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -69,10 +76,6 @@ public function testCallToIncorrectCaseFunctionName(): void public function testMatchExprAnalysis(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/match-expr-analysis.php'], [ [ 'Function lorem not found.', @@ -97,12 +100,61 @@ public function testMatchExprAnalysis(): void ]); } - public function testCreateFunctionPhp8(): void + #[RequiresPhp('>= 8.0')] + 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', + ], + ]); + } + #[RequiresPhp('>= 8.0')] + public function testCreateFunctionPhp8(): void + { $this->analyse([__DIR__ . '/data/create_function.php'], [ [ 'Function create_function not found.', @@ -112,13 +164,99 @@ public function testCreateFunctionPhp8(): void ]); } + #[RequiresPhp('< 8.0')] 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'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug8058(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-8058.php'], []); + } + + #[RequiresPhp('< 8.2')] + public function testBug8058b(): void + { + $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..ba44a5bc0f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -0,0 +1,105 @@ + + */ +class CallUserFuncRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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)); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testNoNamedArguments(): void + { + $this->analyse([__DIR__ . '/data/no-named-arguments-call-user-func.php'], [ + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 29, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 30, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 31, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 32, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index ba897cdaa6..659659449e 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class ClosureAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ClosureAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/closure-attributes.php'], [ [ 'Attribute class ClosureAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index addb8aaf1a..38d4ec65d8 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(self::createReflectionProvider(), true, false, true, false, false, false, true))); } public function testClosureReturnTypeRule(): void @@ -28,19 +30,19 @@ 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.', + 'Anonymous function should return array{}|null but empty return statement found.', 88, ], [ @@ -91,4 +93,44 @@ public function testBug3891(): void $this->analyse([__DIR__ . '/data/bug-3891.php'], []); } + public function testBug6806(): void + { + $this->analyse([__DIR__ . '/data/bug-6806.php'], []); + } + + public function testBug4739(): void + { + $this->analyse([__DIR__ . '/data/bug-4739.php'], []); + } + + public function testBug4739b(): void + { + $this->analyse([__DIR__ . '/data/bug-4739b.php'], []); + } + + public function testBug5753(): void + { + $this->analyse([__DIR__ . '/data/bug-5753.php'], []); + } + + public function testBug6559(): void + { + $this->analyse([__DIR__ . '/data/bug-6559.php'], []); + } + + public function testBug6902(): void + { + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + public function testBug7220(): void + { + $this->analyse([__DIR__ . '/data/bug-7220.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/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 4c01c5a4fe..0ca5ca5013 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -4,28 +4,47 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInArrowFunctionTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = self::createReflectionProvider(); + return new ExistingClassesInArrowFunctionTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + new PhpVersion(PHP_VERSION_ID), + ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/arrow-function-typehints.php'], [ [ 'Parameter $bar of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Bar.', @@ -38,7 +57,7 @@ public function testRule(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -62,26 +81,34 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataNativeUnionTypes')] public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } - public function dataRequiredParameterAfterOptional(): array + public static 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, @@ -98,24 +125,218 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 11, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 19, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 23, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], ], ], ]; } /** - * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataRequiredParameterAfterOptional')] public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional-arrow.php'], $errors); } + public static 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, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataIntersectionTypes')] + 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); + } + + public function testBug5206(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + 'Parameter $mixed of anonymous function has invalid type Bug5206\mixed.', + 9, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-5206.php'], $errors); + } + + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/arrow-function-typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void anonymous function.', + 10, + ], + [ + 'Attribute NoDiscard cannot be used on never anonymous function.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index d7252b4ca4..1fa4bd16dc 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -4,21 +4,42 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClosureTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = self::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 @@ -65,9 +86,6 @@ public function testValidTypehintPhp71(): void ]); } - /** - * @requires PHP 7.2 - */ public function testValidTypehintPhp72(): void { $this->analyse([__DIR__ . '/data/closure-7.2-typehints.php'], []); @@ -75,9 +93,6 @@ public function testValidTypehintPhp72(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of anonymous function has invalid type void.', @@ -86,7 +101,7 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -110,26 +125,34 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataNativeUnionTypes')] public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } - public function dataRequiredParameterAfterOptional(): array + public static 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, @@ -146,20 +169,203 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 17, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], ], ], ]; } /** - * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataRequiredParameterAfterOptional')] public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional-closures.php'], $errors); } + public static 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, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataIntersectionTypes')] + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/closure-intersection-types.php'], $errors); + } + + #[RequiresPhp('>= 8.4')] + public function testDeprecatedImplicitlyNullableParameterType(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/closure-typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void anonymous function.', + 5, + ], + [ + 'Attribute NoDiscard cannot be used on never anonymous function.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 859b7a2e12..79beed0ea4 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -4,22 +4,42 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = self::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 @@ -154,9 +174,6 @@ public function testWithoutNamespace(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of function VoidParameterTypehint\doFoo() has invalid type void.', @@ -165,7 +182,7 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -189,26 +206,34 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataNativeUnionTypes')] public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } - public function dataRequiredParameterAfterOptional(): array + public static 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, @@ -225,20 +250,258 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 18, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 38, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 46, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 50, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], ], ], ]; } /** - * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataRequiredParameterAfterOptional')] public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional.php'], $errors); } + public static 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, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataIntersectionTypes')] + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + + 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); + } + + #[RequiresPhp('>= 8.0')] + public function testConditionalReturnType(): void + { + $this->analyse([__DIR__ . '/data/conditional-return-type.php'], [ + [ + 'Template type T of function FunctionConditionalReturnType\notGet() is not referenced in a parameter.', + 17, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type S of function ParamOutTemplate\uselessGeneric() is not referenced in a parameter.', + 9, + ], + ]); + } + + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void function TestFunctionTypehints\nothing().', + 6, + ], + [ + 'Attribute NoDiscard cannot be used on never function TestFunctionTypehints\returnNever().', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 799e1a0b90..0ad48b3e98 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class FunctionAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new FunctionAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/function-attributes.php'], [ [ 'Attribute class FunctionAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php new file mode 100644 index 0000000000..9db721eaee --- /dev/null +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -0,0 +1,74 @@ + + */ +class FunctionCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new FunctionCallableRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new PhpVersion(PHP_VERSION_ID), + true, + true, + ); + } + + #[RequiresPhp('< 8.1')] + public function testNotSupportedOnOlderVersions(): void + { + $this->analyse([__DIR__ . '/data/function-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-callable.php'], [ + [ + 'Function nonexistent not found.', + 13, + ], + [ + 'Creating callable from string but it might not be a callable.', + 19, + ], + [ + 'Creating callable from 1 but it\'s not a callable.', + 33, + ], + [ + 'Call to function strlen() with incorrect case: StrLen', + 38, + ], + [ + 'Creating callable from 1|(callable(): mixed) but it might not be a callable.', + 47, + ], + [ + 'Creating callable from an unknown class FunctionCallable\Nonexistent.', + 52, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php deleted file mode 100644 index a06fbb4fc3..0000000000 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ -class ImplodeFunctionRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - $broker = $this->createReflectionProvider(); - return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false)); - } - - public function testFile(): void - { - $this->analyse([__DIR__ . '/data/implode.php'], [ - [ - 'Parameter #2 $array of function implode expects array, array|string> given.', - 9, - ], - [ - 'Parameter #1 $array of function implode expects array, array> given.', - 11, - ], - [ - 'Parameter #1 $array of function implode expects array, array> given.', - 12, - ], - [ - 'Parameter #1 $array of function implode expects array, array> given.', - 13, - ], - [ - 'Parameter #2 $array of function implode expects array, array> given.', - 15, - ], - [ - 'Parameter #2 $array of function join expects array, array> given.', - 16, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..d0f1b9a809 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -0,0 +1,107 @@ + + */ +class ImplodeParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::createReflectionProvider(); + return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArguments(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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 5e17b2b675..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, true)); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -35,52 +36,63 @@ public function testRule(): void [ 'Function MissingFunctionParameterTypehint\missingArrayTypehint() has parameter $a with no value type specified in iterable type array.', 36, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingPhpDocIterableTypehint() has parameter $a with no value type specified in iterable type array.', 44, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 60, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\acceptsGenericInterface() has parameter $i with generic interface MissingFunctionParameterTypehint\GenericInterface but does not specify its types: T, U', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\acceptsGenericClass() has parameter $c with generic class MissingFunctionParameterTypehint\GenericClass but does not specify its types: A, B', 130, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehint() has parameter $iterable with no value type specified in iterable type iterable.', 135, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehintPhpDoc() has parameter $iterable with no value type specified in iterable type iterable.', 143, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehint() has parameter $traversable with no value type specified in iterable type Traversable.', 148, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehintPhpDoc() has parameter $traversable with no value type specified in iterable type Traversable.', 156, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.', 161, ], + [ + 'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 173, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 181, + ], + [ + 'Function MissingParamClosureThisType\generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 191, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index eb26d2d90e..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, true)); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -31,22 +32,19 @@ public function testRule(): void [ 'Function MissingFunctionReturnTypehint\unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 51, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionReturnTypehint\returnsGenericInterface() return type with generic interface MissingFunctionReturnTypehint\GenericInterface does not specify its types: T, U', 70, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\returnsGenericClass() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\genericGenericMissingTemplateArgs() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 105, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\closureWithNoPrototype() return type has no signature specified for Closure.', diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index e64960fdbe..0b0296bc6a 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class ParamAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ParamAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/param-attributes.php'], [ [ 'Attribute class ParamAttributes\Foo does not have the parameter target.', @@ -61,4 +65,14 @@ public function testRule(): void ]); } + public function testSensitiveParameterAttribute(): void + { + $this->analyse([__DIR__ . '/data/sensitive-parameter.php'], []); + } + + public function testBug10298(): void + { + $this->analyse([__DIR__ . '/data/bug-10298.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php new file mode 100644 index 0000000000..588c845952 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -0,0 +1,168 @@ + + */ +class ParameterCastableToNumberRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::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, + ], + ])); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArguments(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11883(): void + { + $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..bfa7339c18 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -0,0 +1,245 @@ + + */ +class ParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::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, + ], + ])); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArguments(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11111(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11141(): void + { + $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..d799e58579 --- /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)), + self::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/PrintfParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php new file mode 100644 index 0000000000..06e34b998c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php @@ -0,0 +1,252 @@ + + */ +class PrintfParameterTypeRuleTest extends RuleTestCase +{ + + private bool $checkStrictPrintfPlaceholderTypes = false; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new PrintfParameterTypeRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $reflectionProvider, + new RuleLevelHelper( + $reflectionProvider, + true, + false, + true, + true, + true, + true, + false, + ), + $this->checkStrictPrintfPlaceholderTypes, + ); + } + + public function test(): void + { + $this->analyse([__DIR__ . '/data/printf-param-types.php'], [ + [ + 'Parameter #2 of function printf is expected to be castable to int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.', + 15, + ], + [ + 'Parameter #2 of function printf is expected to be castable to int by placeholder #1 ("%d"), int|PrintfParamTypes\\FooStringable given.', + 16, + ], + [ + 'Parameter #2 of function printf is expected to be castable to float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.', + 17, + ], + [ + 'Parameter #2 of function sprintf is expected to be castable to int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.', + 18, + ], + [ + 'Parameter #3 of function fprintf is expected to be castable to float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.', + 19, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), string given.', + 20, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), float given.', + 21, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), SimpleXMLElement given.', + 22, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), null given.', + 23, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), true given.', + 24, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%.*s" (precision)), string given.', + 25, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #2 ("%3$.*s" (precision)), string given.', + 26, + ], + [ + 'Parameter #2 of function printf is expected to be castable to float by placeholder #1 ("%1$-\'X10.2f"), PrintfParamTypes\\FooStringable given.', + 27, + ], + [ + 'Parameter #2 of function printf is expected to be castable to float by placeholder #2 ("%1$*.*f" (value)), PrintfParamTypes\\FooStringable given.', + 28, + ], + [ + 'Parameter #4 of function printf is expected to be castable to float by placeholder #1 ("%3$f"), PrintfParamTypes\\FooStringable given.', + 29, + ], + [ + 'Parameter #2 of function printf is expected to be castable to float by placeholder #1 ("%1$f"), PrintfParamTypes\\FooStringable given.', + 30, + ], + [ + 'Parameter #2 of function printf is expected to be castable to int by placeholder #2 ("%1$d"), PrintfParamTypes\\FooStringable given.', + 30, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (width)), float given.', + 31, + ], + ]); + } + + public function testStrict(): void + { + $this->checkStrictPrintfPlaceholderTypes = true; + $this->analyse([__DIR__ . '/data/printf-param-types.php'], [ + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.', + 15, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), int|PrintfParamTypes\\FooStringable given.', + 16, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.', + 17, + ], + [ + 'Parameter #2 of function sprintf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.', + 18, + ], + [ + 'Parameter #3 of function fprintf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.', + 19, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), string given.', + 20, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), float given.', + 21, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), SimpleXMLElement given.', + 22, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), null given.', + 23, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), true given.', + 24, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%.*s" (precision)), string given.', + 25, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #2 ("%3$.*s" (precision)), string given.', + 26, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$-\'X10.2f"), PrintfParamTypes\\FooStringable given.', + 27, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #2 ("%1$*.*f" (value)), PrintfParamTypes\\FooStringable given.', + 28, + ], + [ + 'Parameter #4 of function printf is expected to be float by placeholder #1 ("%3$f"), PrintfParamTypes\\FooStringable given.', + 29, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$f"), PrintfParamTypes\\FooStringable given.', + 30, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #2 ("%1$d"), PrintfParamTypes\\FooStringable given.', + 30, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (width)), float given.', + 31, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (value)), float given.', + 31, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float given.', + 34, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float|int given.', + 35, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.', + 36, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.', + 37, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), null given.', + 38, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), true given.', + 39, + ], + [ + 'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), SimpleXMLElement given.', + 40, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), string given.', + 42, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), null given.', + 43, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), true given.', + 44, + ], + [ + 'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), SimpleXMLElement given.', + 45, + ], + [ + 'Parameter #2 of function printf is expected to be string by placeholder #1 ("%s"), null given.', + 47, + ], + [ + 'Parameter #2 of function printf is expected to be string by placeholder #1 ("%s"), true given.', + 48, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index b89c61e71c..d60b1e0be6 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -3,17 +3,22 @@ namespace PHPStan\Rules\Functions; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class PrintfParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class PrintfParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new PrintfParametersRule(new PhpVersion(PHP_VERSION_ID)); + return new PrintfParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + self::createReflectionProvider(), + ); } public function testFile(): void @@ -108,4 +113,14 @@ public function testBug4717(): void $this->analyse([__DIR__ . '/data/bug-4717.php'], $errors); } + public function testBug2342(): void + { + $this->analyse([__DIR__ . '/data/bug-2342.php'], [ + [ + 'Call to sprintf contains 1 placeholder, 0 values given.', + 5, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php index 06d0f8a03a..39160f054f 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(self::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 index 9d31dc75d2..46a1e0c17f 100644 --- a/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,10 +20,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/return-null-safe-by-ref.php'], [ [ 'Nullsafe cannot be returned by reference.', @@ -39,4 +36,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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 9425f462b6..c1fac8e311 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::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.', @@ -66,6 +74,8 @@ public function testReturnTypeRule(): void public function testReturnTypeRulePhp70(): void { + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/returnTypes-7.0.php'], [ [ 'Function ReturnTypes\Php70\returnInteger() should return int but empty return statement found.', @@ -76,27 +86,24 @@ public function testReturnTypeRulePhp70(): void public function testIsGenerator(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/is-generator.php'], []); } public function testBug2568(): void { require_once __DIR__ . '/data/bug-2568.php'; - $this->analyse([__DIR__ . '/data/bug-2568.php'], [ - [ - 'Function Bug2568\my_array_keys() should return array but returns array.', - 12, - ], - ]); + $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.', @@ -105,4 +112,277 @@ public function testBug2723(): void ]); } + public function testBug5706(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5706.php'], []); + } + + public function testBug5844(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5844.php'], []); + } + + public function testBug7218(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7218.php'], []); + } + + public function testBug5751(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5751.php'], []); + } + + public function testBug3931(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-3931.php'], []); + } + + public function testBug3801(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-3801.php'], [ + [ + 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, true}.', + 17, + '• Type #1 from the union: Offset 1 (null) does not accept type true. +• Type #2 from the union: Offset 0 (null) does not accept type false.', + ], + [ + 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, false}.', + 21, + '• Type #1 from the union: Offset 1 (null) does not accept type false. +• Type #2 from the union: Offset 0 (null) does not accept type false.', + ], + ]); + } + + public function testListWithNullablesChecked(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/return-list-nullables.php'], [ + [ + 'Function ReturnListNullables\doFoo() should return array|null but returns list.', + 16, + ], + ]); + } + + public function testListWithNullablesUnchecked(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = false; + $this->analyse([__DIR__ . '/data/return-list-nullables.php'], []); + } + + public function testBug6787(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6787.php'], [ + [ + 'Function Bug6787\f() should return T of DateTimeInterface but returns DateTime.', + 11, + 'Type DateTime is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug6568(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6568.php'], [ + [ + 'Function Bug6568\test() should return T of array but returns array.', + 12, + 'Type array is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug7766(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7766.php'], [ + [ + "Function Bug7766\problem() should return array but returns array{array{id: 1, created: DateTimeImmutable, updated: DateTimeImmutable, valid_from: DateTimeImmutable, valid_till: DateTimeImmutable, string: 'string', other_string: 'string', another_string: 'string', ...}}.", + 20, + "Offset 'count' (int<0, max>) does not accept type '4'.", + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8846(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8846.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10077(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7984(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7984.php'], []); + } + + public function testBug5594(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5594.php'], []); + } + + public function testBug5592(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5592.php'], []); + } + + 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 testBug11917(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11917.php'], []); + } + + 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.', + ], + ]); + } + + public function testBug13484(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + + $this->analyse([__DIR__ . '/data/bug-13484.php'], []); + } + + public function testBug9401(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + + $this->analyse([__DIR__ . '/data/bug-9401.php'], [ + [ + 'Function Bug9401\foo() should return list> but returns list>.', + 17, + ], + ]); + } + + public function testBug12973(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12973.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..8193d5b6ef --- /dev/null +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -0,0 +1,184 @@ + + */ +class SortParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::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, + ], + ])); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArguments(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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 8152274a6a..137257162c 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($this->createReflectionProvider())); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck(self::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..c71f59ec2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php @@ -0,0 +1,51 @@ + + */ +class UselessFunctionReturnValueRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UselessFunctionReturnValueRule( + self::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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testUselessReturnValuePhp8(): void + { + $this->analyse([__DIR__ . '/data/useless-fn-return-php8.php'], [ + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php new file mode 100644 index 0000000000..7955385f36 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php @@ -0,0 +1,37 @@ + + */ +class VariadicParametersDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VariadicParametersDeclarationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/variadic-parameters-declaration.php'], [ + [ + 'Only the last parameter can be variadic.', + 7, + ], + [ + 'Only the last parameter can be variadic.', + 11, + ], + [ + 'Only the last parameter can be variadic.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php new file mode 100644 index 0000000000..e50e81894c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php @@ -0,0 +1,7 @@ += 8.4 + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_all($array, fn ($value, $key) => $key === 0); + + // ok + array_all($array, fn (string $value, int $key) => $key === 0); + + // ok + array_all($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_all($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_all($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_any.php b/tests/PHPStan/Rules/Functions/data/array_any.php new file mode 100644 index 0000000000..1c267ffc62 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_any.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_any($array, fn ($value, $key) => $key === 0); + + // ok + array_any($array, fn (string $value, int $key) => $key === 0); + + // ok + array_any($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_any($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_any($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php new file mode 100644 index 0000000000..257fc17c85 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php new file mode 100644 index 0000000000..8a98630a88 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_empty.php b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php new file mode 100644 index 0000000000..3bcb4dd6ea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php @@ -0,0 +1,28 @@ + $objectsOrNull */ +$objectsOrNull = []; +/** @var array $falsey */ +$falsey = []; + +array_filter([0,1,3]); +array_filter([1,3]); +array_filter(['test']); +array_filter(['', 'test']); +array_filter([null, 'test']); +array_filter([false, 'test']); +array_filter([true, false]); +array_filter([true, true]); +array_filter([new \stdClass()]); +array_filter([new \stdClass(), null]); +array_filter($objects); +array_filter($objectsOrNull); + +array_filter([0]); +array_filter([null]); +array_filter([null, null]); +array_filter([null, 0]); +array_filter($falsey); +array_filter([]); diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php b/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php new file mode 100644 index 0000000000..9acd434233 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php @@ -0,0 +1,3 @@ += 8.4 + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find($array, fn ($value, $key) => $key === 0); + + // ok + array_find($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find($array, fn (string $value) => $key === 0); + + // bad parameters + array_find($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php new file mode 100644 index 0000000000..ab2b7df3fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find_key($array, fn ($value, $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_find_key($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find_key($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php new file mode 100644 index 0000000000..9f8ba1bfa3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php new file mode 100644 index 0000000000..4d81086d24 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_map_multiple.php b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php index 06d872a41f..0d96ce95e9 100644 --- a/tests/PHPStan/Rules/Functions/data/array_map_multiple.php +++ b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php @@ -60,4 +60,9 @@ public function doFoo(): void }, [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_arrow.php b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php index 0606d7a275..d93d7b6ffc 100644 --- a/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + $b; + }, +); + +array_udiff( + ["25","26"], + ["26","27"], + 'strcasecmp', +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php new file mode 100644 index 0000000000..7827734e59 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php new file mode 100644 index 0000000000..f4767621c2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_udiff_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect.php b/tests/PHPStan/Rules/Functions/data/array_uintersect.php new file mode 100644 index 0000000000..dbe5307a82 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect.php @@ -0,0 +1,33 @@ + $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + ['a', 'b'], + ['c', 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php new file mode 100644 index 0000000000..e410f3827e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php @@ -0,0 +1,33 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php new file mode 100644 index 0000000000..f079aec189 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_uintersect_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_values_list.php b/tests/PHPStan/Rules/Functions/data/array_values_list.php new file mode 100644 index 0000000000..7eef89c9cd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_values_list.php @@ -0,0 +1,28 @@ + $list */ +$list = [1, 2, 3]; +/** @var list $list */ +$array = ['a' => 1, 'b' => 2, 'c' => 3]; + +array_values([0,1,3]); +array_values([1,3]); +array_values(['test']); +array_values(['a' => 'test']); +array_values(['', 'test']); +array_values(['a' => '', 'b' => 'test']); +array_values($list); +array_values($array); + +array_values([0]); +array_values(['a' => null, 'b' => null]); +array_values([null, null]); +array_values([null, 0]); +array_values([]); + +/** @var array{} $empty */ +$empty = doFoo(); +array_values($empty); + +array_values(unused: true, array: $array); +array_values(unused: true, array: $list); diff --git a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php index ca75b4998b..22a69296f1 100644 --- a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + 1, 'bar' => 2]; array_walk( diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php index fa1ec0f17a..30f1a93884 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php @@ -1,4 +1,4 @@ -= 7.4 += 8.1 + +namespace 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-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php new file mode 100644 index 0000000000..b8f7115d68 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php @@ -0,0 +1,18 @@ + true; + } + + public function doBar() + { + #[\NoDiscard] fn(): never => true; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php index 1ff3081dc5..29886b7dab 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php @@ -1,4 +1,4 @@ -= 7.4 + yield $value; diff --git a/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php new file mode 100644 index 0000000000..b5a3c485c8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php @@ -0,0 +1,87 @@ + $v) { + trim($k); + } + + foreach ($_SERVER as $k => $v) { + trim($k); + } + + foreach ($_GET as $k => $v) { + trim($k); + } + + foreach ($_POST as $k => $v) { + trim($k); + } + + foreach ($_FILES as $k => $v) { + trim($k); + } + + foreach ($_COOKIE as $k => $v) { + trim($k); + } + + foreach ($_SESSION as $k => $v) { + trim($k); + } + + foreach ($_REQUEST as $k => $v) { + trim($k); + } + + foreach ($_ENV as $k => $v) { + trim($k); + } +} + +function benevolentKeysOfSuperglobalsInt(): void +{ + foreach ($GLOBALS as $k => $v) { + acceptInt($k); + } + + foreach ($_SERVER as $k => $v) { + acceptInt($k); + } + + foreach ($_GET as $k => $v) { + acceptInt($k); + } + + foreach ($_POST as $k => $v) { + acceptInt($k); + } + + foreach ($_FILES as $k => $v) { + acceptInt($k); + } + + foreach ($_COOKIE as $k => $v) { + acceptInt($k); + } + + foreach ($_SESSION as $k => $v) { + acceptInt($k); + } + + foreach ($_REQUEST as $k => $v) { + acceptInt($k); + } + + foreach ($_ENV as $k => $v) { + acceptInt($k); + } +} + +function acceptInt(int $i): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10003.php b/tests/PHPStan/Rules/Functions/data/bug-10003.php new file mode 100644 index 0000000000..af669a63ed --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10003.php @@ -0,0 +1,16 @@ +doLoad(false, \func_get_args()); +} + +$projectDir = __DIR__; +$environment = 'dev'; + +$files = array_reverse(array_filter([ + $projectDir.'/.env', + $projectDir.'/.env.'.$environment, + $projectDir.'/.env.local', + $projectDir.'/.env.'.$environment.'.local', +], 'file_exists')); + +if ($files !== []) { + load(...$files); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10077.php b/tests/PHPStan/Rules/Functions/data/bug-10077.php new file mode 100644 index 0000000000..c926cf291e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10077.php @@ -0,0 +1,57 @@ += 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-12224.php b/tests/PHPStan/Rules/Functions/data/bug-12224.php new file mode 100644 index 0000000000..9970576bca --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12224.php @@ -0,0 +1,19 @@ +uuid; } +} + +class HelloWorld +{ + /** + * @param list $arr + */ + public function sayHello(array $arr): void + { + $callback = static fn(Uuid $uuid): string => (string) $uuid; + + // ok + array_map(array: $arr, callback: $callback); + array_map(callback: $callback, array: $arr); + array_map($callback, $arr); + array_map($callback, array: $arr); + array_map(static fn (Uuid $u1, Uuid $u2): string => (string) $u1, $arr, $arr); + + // should be reported + $invalidCallback = static fn(string $uuid): string => $uuid; + array_map($invalidCallback, $arr); + array_map(array: $arr, callback: $invalidCallback); + } +} 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-13065.php b/tests/PHPStan/Rules/Functions/data/bug-13065.php new file mode 100644 index 0000000000..f63acf9989 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13065.php @@ -0,0 +1,15 @@ +arguments = $arguments; + } +} + +function test(): MyPeriod +{ + return new MyPeriod(); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-13556.php b/tests/PHPStan/Rules/Functions/data/bug-13556.php new file mode 100644 index 0000000000..17bd304ede --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13556.php @@ -0,0 +1,7 @@ +real_connect( + null, + null, + null, + null, + null, + null, + \MYSQLI_CLIENT_SSL + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2911.php b/tests/PHPStan/Rules/Functions/data/bug-2911.php new file mode 100644 index 0000000000..194b8a3c0a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2911.php @@ -0,0 +1,31 @@ + $array + */ +function foo(array $array): void { + $array['bar'] = 'string'; + + // 'bar' is always set, should not complain here + bar($array); +} + + +/** + * @param array $array + */ +function foo2(array $array): void { + $array['foo'] = 'string'; + + // 'bar' is always set, should not complain here + bar($array); +} + + +/** + * @param array{bar: string} $array + */ +function bar(array $array): void { +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3107.php b/tests/PHPStan/Rules/Functions/data/bug-3107.php new file mode 100644 index 0000000000..12ed0edfd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3107.php @@ -0,0 +1,23 @@ +val = $mixed; + + $a = []; + $a[$holder->val] = 1; + take($a); +} + +/** @param array $a */ +function take($a): void {} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3261.php b/tests/PHPStan/Rules/Functions/data/bug-3261.php index 5e5c5f874e..b5fc443ff0 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-3261.php +++ b/tests/PHPStan/Rules/Functions/data/bug-3261.php @@ -1,4 +1,4 @@ -= 7.4 +dummy(function(int $a, ...$args) : void{ + var_dump(...$args); + }); + } +} 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 @@ + */ + const FACTORIES = [ + 'a' => [Factory::class, 'a'], + 'b' => [Factory::class, 'b'] + ]; + + public function withLiteral(): void + { + (self::FACTORIES['a'])(); + } + + public function withVariable(string $id): void + { + if (!isset(self::FACTORIES[$id])) { + return; + } + + (self::FACTORIES[$id])(); + } +} + +class HelloWorld2 +{ + const FACTORIES = [ + 'a' => [Factory::class, 'a'], + 'b' => [Factory::class, 'b'] + ]; + + public function withLiteral(): void + { + (self::FACTORIES['a'])(); + } + + public function withVariable(string $id): void + { + if (!isset(self::FACTORIES[$id])) { + return; + } + + (self::FACTORIES[$id])(); + } +} 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 index c42021e7dd..6eb3bf468f 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-3660.php +++ b/tests/PHPStan/Rules/Functions/data/bug-3660.php @@ -1,4 +1,4 @@ -= 7.4 +handleA(...) : $this->handleB(...); + + $method($obj); + } + + private function handleA(A $a): void + { + } + + private function handleB(B $b): void + { + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3931.php b/tests/PHPStan/Rules/Functions/data/bug-3931.php new file mode 100644 index 0000000000..424c7ca236 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3931.php @@ -0,0 +1,26 @@ + $arr + * @return void + */ +function test(array $arr): void +{ + $r = addSomeKey($arr, 1); + assertType("array{mykey: int}", $r); // could be better, the T part currently disappears +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3946.php b/tests/PHPStan/Rules/Functions/data/bug-3946.php new file mode 100644 index 0000000000..bdb12cccb0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3946.php @@ -0,0 +1,8 @@ + $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-4612.php b/tests/PHPStan/Rules/Functions/data/bug-4612.php new file mode 100644 index 0000000000..3f8c3f6bd5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4612.php @@ -0,0 +1,16 @@ + $array */ +$array = []; + +foreach ($array as $k => $v) { + if (check($k) && isset($prev)) { + $array[$prev] = $v; + } + + $prev = $k; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4739.php b/tests/PHPStan/Rules/Functions/data/bug-4739.php new file mode 100644 index 0000000000..e8d0fdfbaa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4739.php @@ -0,0 +1,19 @@ + $value) { + if ($predicate($value)) { + yield $key => $value; + } + } +} + + +class Record { + /** + * @var boolean + */ + public $isInactive; + /** + * @var string + */ + public $name; +} + +function doFoo() { + $emails = []; + $records = []; + filter( + function (Record $domain) use (&$emails): bool { + if (!isset($emails[$domain->name])) { + $emails[$domain->name] = TRUE; + return TRUE; + } + return !$domain->isInactive; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-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-5206.php b/tests/PHPStan/Rules/Functions/data/bug-5206.php new file mode 100644 index 0000000000..58738ac19a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5206.php @@ -0,0 +1,11 @@ + '`mixed` type!'; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5288.php b/tests/PHPStan/Rules/Functions/data/bug-5288.php new file mode 100644 index 0000000000..a116082737 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5288.php @@ -0,0 +1,54 @@ +get_iterator(); + $data = array_map( + function ($value): void {}, + iterator_to_array($iterator) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5356.php b/tests/PHPStan/Rules/Functions/data/bug-5356.php index 158e2f7532..c5123a08d1 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-5356.php +++ b/tests/PHPStan/Rules/Functions/data/bug-5356.php @@ -1,4 +1,4 @@ -= 7.4 + $map + * @return numeric-string + */ +function mapGet(\Ds\Map $map, \Ds\Hashable $key): string +{ + return $map->get($key, '0'); +} + +/** + * @template TDefault + * @param TDefault $default + * @return numeric-string|TDefault + */ +function getFooOrDefault($default) { + if ((bool) random_int(0, 1)) { + /** @var numeric-string */ + $foo = '5'; + return $foo; + } else { + return $default; + } +} + +function doStuff(): int +{ + /** + * @var \Ds\Map + */ + $map = new \Ds\Map(); + + return $map->get('foo', 1); +} + +/** + * @return numeric-string + */ +function doStuff1(): string { + /** @var numeric-string */ + $foo = '12'; + return getFooOrDefault($foo); +} + +/** + * @return numeric-string + */ +function doStuff2(): string { + return getFooOrDefault('12'); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5594.php b/tests/PHPStan/Rules/Functions/data/bug-5594.php new file mode 100644 index 0000000000..19dd58ed83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5594.php @@ -0,0 +1,14 @@ + + */ +function createIterator(array $items): ArrayIterator +{ + return new ArrayIterator($items); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-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 @@ +|null $arrayOrNull + */ +function doImplode(?array $arrayOrNull): void +{ + join(',', $arrayOrNull); + join($arrayOrNull); + + implode(',', $arrayOrNull); + implode($arrayOrNull); +} + +/** + * @param array|string $union + */ +function more(array|string $union): void +{ + join(',', $union); + join($union); + + implode(',', $union); + implode($union); +} + +function success(): void +{ + join(',', ['']); + join(['']); + + implode(',', ['']); + implode(['']); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5834.php b/tests/PHPStan/Rules/Functions/data/bug-5834.php new file mode 100644 index 0000000000..5cb124c2bf --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5834.php @@ -0,0 +1,28 @@ +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-7772.php b/tests/PHPStan/Rules/Functions/data/bug-7772.php new file mode 100644 index 0000000000..00d4272f85 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7772.php @@ -0,0 +1,14 @@ += 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-8506.php b/tests/PHPStan/Rules/Functions/data/bug-8506.php new file mode 100644 index 0000000000..be6d538329 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8506.php @@ -0,0 +1,18 @@ + + */ +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-9401.php b/tests/PHPStan/Rules/Functions/data/bug-9401.php new file mode 100644 index 0000000000..a818188f31 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9401.php @@ -0,0 +1,18 @@ + $foos + * @return list + */ +function foo(array $foos): array +{ + $list = []; + foreach ($foos as $foo) { + if (is_int($foo) && $foo >= 0) { + $list[] = $foo; + } + } + return $list; +} 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-9970.php b/tests/PHPStan/Rules/Functions/data/bug-9970.php new file mode 100644 index 0000000000..8aa447e2f4 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9970.php @@ -0,0 +1,39 @@ +dummy = "b"; + throw new \Exception("ex"); + } + + function endHandler($XmlParser, $tag) + { + } +} + +$p1 = new Xml_Parser(); +try { + $p1->parse(''); + echo "Exception swallowed\n"; +} catch (\Exception $e) { + echo "OK\n"; +} 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-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-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-nullsafe.php b/tests/PHPStan/Rules/Functions/data/callables-nullsafe.php new file mode 100644 index 0000000000..8cba81fa7f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/callables-nullsafe.php @@ -0,0 +1,20 @@ += 8.0 + +namespace CallablesNullsafe; + +class Bar +{ + + public int $val; + +} + +function doFoo(?Bar $bar): void +{ + $fn = function (int $val) { + + }; + + $fn($bar?->val); +} + diff --git a/tests/PHPStan/Rules/Functions/data/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-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php new file mode 100644 index 0000000000..ca152e2340 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php @@ -0,0 +1,13 @@ + ? T : mixed) + */ +function get(string $id): mixed +{ +} + +/** + * @template T + * @return ($id is not class-string ? T : mixed) + */ +function notGet(string $id): mixed +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/count-array-shift.php b/tests/PHPStan/Rules/Functions/data/count-array-shift.php new file mode 100644 index 0000000000..fcbb82b2ae --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/count-array-shift.php @@ -0,0 +1,19 @@ +|false $a */ +function foo($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} + +/** @param non-empty-array|false $a */ +function bar($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php new file mode 100644 index 0000000000..bc5b8f6cea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -0,0 +1,84 @@ + 'bar')); + curl_setopt($curl, CURLOPT_POSTFIELDS, ''); + curl_setopt($curl, CURLOPT_POSTFIELDS, 'para1=val1¶2=val2'); + curl_setopt($curl, CURLOPT_COOKIEFILE, ''); + curl_setopt($curl, CURLOPT_PRE_PROXY, ''); + curl_setopt($curl, CURLOPT_PROXY, ''); + curl_setopt($curl, CURLOPT_PRIVATE, ''); + curl_setopt($curl, CURLOPT_ENCODING, ''); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, ''); + } + + public function bug9263() { + $curl = curl_init(); + + $header_dictionary = [ + 'Accept' => 'application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_dictionary); + + $header_list = [ + 'Accept: application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_list); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/discussion-10454.php b/tests/PHPStan/Rules/Functions/data/discussion-10454.php new file mode 100644 index 0000000000..67adaf0dd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/discussion-10454.php @@ -0,0 +1,29 @@ + $_POST['policy'], // shouldn't this and the next line be an unsafe offset access? + 'entitlements' => $_POST['entitlements'], +]; +assertType('mixed', $_POST['policy']); +assertType('array{policy: mixed, entitlements: mixed}', $args); +foo($args); // I'd expect this to be reported too + +/** @var mixed $mixed */ +$mixed = null; +$args = [ + 'policy' => $mixed, + 'entitlements' => $mixed, +]; +assertType('mixed', $mixed); +assertType('array{policy: mixed, entitlements: mixed}', $args); +foo($args); diff --git a/tests/PHPStan/Rules/Functions/data/duplicate-function.php b/tests/PHPStan/Rules/Functions/data/duplicate-function.php new file mode 100644 index 0000000000..35efa010bf --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/duplicate-function.php @@ -0,0 +1,23 @@ += 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 @@ + 1); + acceptClosure(static fn () => 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php new file mode 100644 index 0000000000..497de17310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php @@ -0,0 +1,36 @@ + [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode($data, JSON_THROW_ON_ERROR), + ], + ])); + file_get_contents($url, false, null); + var_export([]); + var_export([], true); + print_r([]); + print_r([], true); } public function doBar(string $s) diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php new file mode 100644 index 0000000000..e167b4f4b9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -0,0 +1,47 @@ += 8.1 + +namespace FunctionCallStatementResultDiscarded; + +#[\NoDiscard] +function withSideEffects(): array { + echo __FUNCTION__ . "\n"; + return [1]; +} + +withSideEffects(); + +(void)withSideEffects(); + +foreach (withSideEffects() as $num) { + var_dump($num); +} + +#[\nOdISCArD] +function differentCase(): array { + echo __FUNCTION__ . "\n"; + return [1]; +} + +differentCase(); + +$callable = 'FunctionCallStatementResultDiscarded\\withSideEffects'; +$callableResult = $callable(); + +$callable(); + +$firstClassCallable = withSideEffects(...); +$firstClasCallableResult = $firstClassCallable(); + +$firstClassCallable(); + +$closureWithNoDiscard = #[\NoDiscard] function () { return 1; }; +$a = $closureWithNoDiscard(); + +$closureWithNoDiscard(); + +$arrowWithNoDiscard = #[\NoDiscard] fn () => 1; +$b = $arrowWithNoDiscard(); + +$arrowWithNoDiscard(); + +withSideEffects(...); 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-named-parameters.php b/tests/PHPStan/Rules/Functions/data/implode-named-parameters.php new file mode 100644 index 0000000000..8e016b5157 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-named-parameters.php @@ -0,0 +1,10 @@ += 8.1 + +namespace ImplodeParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + implode(',', [FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..362f02ea8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ImplodeParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + // implode weirdness + implode(array: [['a']], separator: ','); + implode(separator: [['a']]); + implode(',', array: [['a']]); + implode(separator: ',', array: [['']]); +} + +function wrongNumberOfArguments(): void +{ + implode(array: ','); + join(array: ','); +} diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php new file mode 100644 index 0000000000..a5723019fa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php @@ -0,0 +1,16 @@ + '1'; + $g = fn (?int $i = null) => '1'; + $h = fn (int $i = 5) => '1'; + $i = fn (int $i = 'foo') => '1'; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php new file mode 100644 index 0000000000..6043b39fc9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php @@ -0,0 +1,24 @@ += 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 @@ + $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +namespace MissingParamClosureThisType { + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php new file mode 100755 index 0000000000..29d9ac8b4e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php @@ -0,0 +1,14 @@ + 2, 'a' => 1], d: 40)); // 46 + +var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php new file mode 100644 index 0000000000..d385b739c9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php @@ -0,0 +1,33 @@ += 8.1 + +namespace NoNamedArgumentsCallUserFunc; + +use function call_user_func; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f): void { + call_user_func(foo(...), i: 1); + call_user_func('NoNamedArgumentsCallUserFunc\\foo', i: 1); + call_user_func([$f, 'doFoo'], i: 1); + call_user_func($f->doFoo(...), i: 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php new file mode 100644 index 0000000000..843530132e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php @@ -0,0 +1,30 @@ += 8.0 + +namespace NoNamedArgumentsFunction; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +function (): void { + foo(i: 5); +}; + +/** + * @param array $a + * @param array $b + * @param array $c + */ +function bar(array $a, array $b, array $c): void +{ + foo(...$a); + foo(...$b); + foo(...$c); + + foo(...[0 => 1]); + foo(...['i' => 1]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php new file mode 100644 index 0000000000..91e9f1f686 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php @@ -0,0 +1,14 @@ += 8.1 + +namespace ParamCastableToNumberFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_sum([FooEnum::A]); + array_product([FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php new file mode 100644 index 0000000000..4fdc546062 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ParamCastableToNumberFunctionsNamedArgs; + +function invalidUsages() +{ + var_dump(array_sum(array: [[0]])); + var_dump(array_product(array: [[0]])); +} + +function validUsages() +{ + var_dump(array_sum(array: [1])); + var_dump(array_product(array: [1])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php new file mode 100644 index 0000000000..9e7c5da4d2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php @@ -0,0 +1,45 @@ +7.7'), 5, 5.5, null])); + var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('7.7'), 5, 5.5, null])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..bbf189cf96 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php @@ -0,0 +1,24 @@ += 8.1 + +namespace ParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_intersect([FooEnum::A], ['a']); + array_intersect(['a'], [FooEnum::A]); + array_intersect(['a'], [], [FooEnum::A]); + array_diff(['a'], [FooEnum::A]); + array_diff_assoc(['a'], [FooEnum::A]); + + array_combine([FooEnum::A], [['b']]); + $arr1 = [FooEnum::A]; + natsort($arr1); + natcasesort($arr1); + array_count_values($arr1); + array_fill_keys($arr1, 5); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..b8790a475d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_combine(values: [['b']], keys: [['a']]); + $arr1 = [['a']]; + array_fill_keys(value: 5, keys: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_combine(values: [[5]]); + array_fill_keys(value: [5]); +} + +function validUsages() +{ + array_combine(values: [['b']], keys: ['a']); + $arr1 = ['a']; + array_fill_keys(value: 5, keys: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php new file mode 100644 index 0000000000..008c2d0142 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php @@ -0,0 +1,63 @@ +7'), 'a'); +printf('%*s', null, 'a'); +printf('%*s', true, 'a'); +printf('%.*s', '5', 'a'); +printf('%2$s %3$.*s', '1', 5, 'a'); // * is the first ordinary placeholder, so it matches '1' +printf('%1$-\'X10.2f', new FooStringable()); +printf('%s %1$*.*f', new FooStringable(), 5, 2); +printf('%3$f', 1, 2, new FooStringable()); +printf('%1$f %1$d', new FooStringable()); +printf('%1$*d', 5.5); + +// Strict error +printf('%d', 1.23); +printf('%d', rand() ? 1.23 : 1); +printf('%d', 'a'); +printf('%d', '1.23'); +printf('%d', null); +printf('%d', true); +printf('%d', new \SimpleXMLElement('aaa')); + +printf('%f', '1.2345678901234567890123456789013245678901234567989'); +printf('%f', null); +printf('%f', true); +printf('%f', new \SimpleXMLElement('aaa')); + +printf('%s', null); +printf('%s', true); + +// Error, but already reported by CallToFunctionParametersRule +printf('%d', new \stdClass()); +printf('%s', []); + +// Error, but already reported by PrintfParametersRule +printf('%s'); +printf('%s', 1, 2); + +// OK +printf('%s', 'a'); +printf('%s', new FooStringable()); +printf('%d', 1); +printf('%f', 1); +printf('%f', 1.1); +printf('%*s', 5, 'a'); +printf('%2$*s', 5, 'a'); +printf('%s %2$*s', 'a', 5, 'a'); +printf('%1$-+\'X10.2f', 5); +printf('%1$*.*f %s %2$d', 5, 6, new FooStringable()); // 5.000000 foo 6 diff --git a/tests/PHPStan/Rules/Functions/data/printf.php b/tests/PHPStan/Rules/Functions/data/printf.php index 7885441c7d..47fdfdab09 100644 --- a/tests/PHPStan/Rules/Functions/data/printf.php +++ b/tests/PHPStan/Rules/Functions/data/printf.php @@ -52,3 +52,25 @@ sprintf($variousPlaceholderCount, 'bar'); sprintf($variousPlaceholderCount, 'bar', 'baz'); sprintf($variousPlaceholderCount, 'bar', 'baz', 'lorem'); + +sprintf('%lc', 1); // ok +sprintf('%ld', 1); // ok +sprintf('%le', 1); // ok +sprintf('%lE', 1); // ok +sprintf('%lf', 1); // ok +sprintf('%lF', 1); // ok +sprintf('%lg', 1); // ok +sprintf('%lG', 1); // ok +sprintf('%lo', 1); // ok +sprintf('%lu', 1); // ok +sprintf('%lx', 1); // ok +sprintf('%lX', 1); // ok + +printf('%0*d', 5, 1); // ok +printf("%'x*d", 5, 1); // ok +printf("%0*d %'x*d", 5, 1, 4, 3); // ok + +printf('%6.*f', 2, 1); // ok +printf('%0*.*f', 6, 2, 1); // ok +printf('%*.*f', 6, 2, 1); // ok +printf('%*.*f %*.*f', 6, 2, 1, 5, 3, 2); // ok 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 @@ += 7.4 += 8.0 namespace RequiredAfterOptional; @@ -9,3 +9,17 @@ fn (int $foo = 1, $bar): int => 1; // not OK fn (bool $foo = true, $bar): int => 1; // not OK + +fn (?int $foo = 1, $bar): int => 1; // not OK + +fn (?int $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = 1, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar): int => 1; // not OK + +fn (mixed $foo = 1, $bar): int => 1; // not OK + +fn (mixed $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): int => 1; // not OK diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php index fdd7db4709..da96ec6909 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -17,3 +17,31 @@ function (int $foo = 1, $bar): void // not OK function(bool $foo = true, $bar): void // not OK { }; + +function (?int $foo = 1, $bar): void // not OK +{ +}; + +function (?int $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = 1, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar): void // not OK +{ +}; + +function (mixed $foo = 1, $bar): void // not OK +{ +}; + +function (mixed $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +}; diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php index 373a441903..8937d262a7 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -22,3 +22,31 @@ function doLorem(bool $foo = true, $bar): void // not OK function doIpsum(bool $foo = true, ...$bar): void // OK { } + +function doDolor(?int $foo = 1, $bar): void // not OK +{ +} + +function doSit(?int $foo = null, $bar): void // not OK +{ +} + +function doAmet(int|null $foo = 1, $bar): void // not OK +{ +} + +function doConsectetur(int|null $foo = null, $bar): void // not OK +{ +} + +function doAdipiscing(mixed $foo = 1, $bar): void // not OK +{ +} + +function doElit(mixed $foo = null, $bar): void // not OK +{ +} + +function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/return-list-nullables.php b/tests/PHPStan/Rules/Functions/data/return-list-nullables.php new file mode 100644 index 0000000000..ae6762061f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/return-list-nullables.php @@ -0,0 +1,17 @@ + $x + * @return array|null + */ +function doFoo(array $x): ?array +{ + $list = []; + foreach ($x as $v) { + $list[] = $v; + } + + return $list; +} diff --git a/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php new file mode 100644 index 0000000000..f902fa9f00 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php @@ -0,0 +1,16 @@ += 8.4 + +namespace ReturnNullSafeByRefPropertyHools; + +use stdClass; + +class Foo +{ + public int $i { + &get { + $foo = new stdClass(); + + return $foo?->foo; + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php b/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php new file mode 100644 index 0000000000..473c143255 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php @@ -0,0 +1,13 @@ += 8.1 + +namespace SortParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages():void +{ + array_unique(['a', FooEnum::A]); + $arr1 = [FooEnum::A]; + sort($arr1, SORT_STRING); + rsort($arr1, SORT_LOCALE_STRING); + asort($arr1, SORT_STRING | SORT_FLAG_CASE); + arsort($arr1, SORT_LOCALE_STRING | SORT_FLAG_CASE); +} + +function validUsages(): void +{ + $arr = [FooEnum::A, 1]; + array_unique($arr, SORT_REGULAR); + sort($arr, SORT_REGULAR); + rsort($arr, 128); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..2a69d861ce --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php @@ -0,0 +1,32 @@ += 8.0 + +namespace SortParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_unique(flags: SORT_STRING, array: [['a'], ['b']]); + $arr1 = [['a']]; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_unique(flags: SORT_STRING); + sort(flags: SORT_STRING); + rsort(flags: SORT_STRING); + asort(flags: SORT_STRING); + arsort(flags: SORT_STRING); +} + +function validUsages() +{ + array_unique(flags: SORT_STRING, array: ['a', 'b']); + $arr1 = ['a']; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php new file mode 100644 index 0000000000..e1e0ff0dca --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php @@ -0,0 +1,71 @@ + static function ($one, $two) { + return $one ?? $two; + }, + 'b' => static function ($one, $two, $three) { + return $one ?? $two ?? $three; + }, + ]; + + foreach (['c', 'd', 'e'] as $name) { + self::$resolvers[$name] = static function ($one, $two) { + return self::$resolvers['a']($one, $two); + }; + + self::$resolvers[$name] = static fn ($one, $two) => self::$resolvers['a']($one, $two); + } + } + + return self::$resolvers; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/true-typehint.php b/tests/PHPStan/Rules/Functions/data/true-typehint.php new file mode 100644 index 0000000000..da84a9cb26 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/true-typehint.php @@ -0,0 +1,8 @@ += 8.2 + +namespace NativeTrueType; + +function alwaysTrue(): true +{ + return true; +} diff --git a/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php new file mode 100644 index 0000000000..554bd8c5c8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php @@ -0,0 +1,11 @@ += 7.4 += 7.4 += 8.0 + +namespace UselessFunctionReturnPhp8; + +class FooClass +{ + public function explicitReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: true, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function explicitNoReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: false, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/useless-fn-return.php b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php new file mode 100644 index 0000000000..204371923b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php @@ -0,0 +1,71 @@ + 1, + 'spracheid' => 2, + ], true) + ); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], true); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ]); + + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool) + ); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + } + + public function missesReturn(): void + { + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function missesReturnVarDump(): string + { + return "Email-Template couldn't be found by parameters:" . var_export([ + 'template' => 1, + 'spracheid' => 2, + ]); + } + + public function explicitNoReturn(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], false) + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/usort_arrow.php b/tests/PHPStan/Rules/Functions/data/usort_arrow.php index d75df390c7..66f079fdbd 100644 --- a/tests/PHPStan/Rules/Functions/data/usort_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/usort_arrow.php @@ -1,4 +1,4 @@ -= 7.4 +format('j. n. Y'); + } + + public function variadicParamAtEnd(int $number, int ...$numbers): void + { + } +} + +function variadicFunction(int ...$a, string $b): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/vprintf.php b/tests/PHPStan/Rules/Functions/data/vprintf.php new file mode 100644 index 0000000000..de3f640310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/vprintf.php @@ -0,0 +1,64 @@ + + * @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(self::createReflectionProvider(), true, false, true, false, false, false, true), true); } public function testRule(): void @@ -37,8 +37,9 @@ 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.', @@ -47,4 +48,9 @@ public function testRule(): void ]); } + public function testBug11517(): void + { + $this->analyse([__DIR__ . '/data/bug-11517.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php index 95ec184fa4..7e234250d8 100644 --- a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class YieldInGeneratorRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 58799d2296..c35ec06136 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(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -45,8 +45,9 @@ 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.', @@ -59,4 +60,15 @@ public function testRule(): void ]); } + public function testBug7484(): void + { + $this->analyse([__DIR__ . '/data/bug-7484.php'], [ + [ + 'Generator expects key type K of int|string, (K of int)|string given.', + 21, + 'Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generators/data/bug-11517.php b/tests/PHPStan/Rules/Generators/data/bug-11517.php new file mode 100644 index 0000000000..56b64a5bd0 --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-11517.php @@ -0,0 +1,30 @@ + + */ + public function bug(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function fine(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function finetoo(): iterable + { + yield from []; + } +} diff --git a/tests/PHPStan/Rules/Generators/data/bug-7484.php b/tests/PHPStan/Rules/Generators/data/bug-7484.php new file mode 100644 index 0000000000..a0d8889a9c --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-7484.php @@ -0,0 +1,23 @@ + $iterable + * @return iterable + */ +function changeKeyCase( + iterable $iterable, + int $case = CASE_LOWER +): iterable { + $callable = $case === CASE_LOWER ? 'strtolower' : 'strtoupper'; + foreach ($iterable as $key => $value) { + if (is_string($key)) { + $key = $callable($key); + } + + yield $key => $value; + } +} diff --git a/tests/PHPStan/Rules/Generators/data/yield.php b/tests/PHPStan/Rules/Generators/data/yield.php index c74eb39013..226f07de3b 100644 --- a/tests/PHPStan/Rules/Generators/data/yield.php +++ b/tests/PHPStan/Rules/Generators/data/yield.php @@ -18,7 +18,7 @@ public function doFoo(): \Generator } /** - * @return\Generator + * @return \Generator */ public function doArrayShape(): \Generator { diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index b31aaa8278..075c3fa6c2 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,14 +15,15 @@ class ClassAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new ClassAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true + new UnresolvableTypeHelper(), + [], + true, ), - new CrossCheckInterfacesHelper() + new CrossCheckInterfacesHelper(), ); } @@ -44,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.', @@ -53,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', @@ -90,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, + ], ]); } @@ -117,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.', @@ -131,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', @@ -176,12 +207,31 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooImplementsGenericInterface implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 198, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in implemented type ClassAncestorsImplements\FooGeneric9 of class ClassAncestorsImplements\FooGeneric10.', 216, ], + [ + 'Class ClassAncestorsImplements\FooIterator @implements tag contains incompatible type ClassAncestorsImplements\FooIterator&iterable.', + 222, + ], + [ + 'Class ClassAncestorsImplements\FooIterator implements generic interface Iterator but does not specify its types: TKey, TValue', + 222, + ], + [ + 'Class ClassAncestorsImplements\FooCollection @implements tag contains incompatible type ClassAncestorsImplements\FooCollection&iterable.', + 235, + ], + [ + 'Class ClassAncestorsImplements\FooCollection implements generic interface ClassAncestorsImplements\AbstractFooCollection but does not specify its types: T', + 235, + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements is not allowed.', + 242, + ], ]); } @@ -220,4 +270,19 @@ public function testScalarClassName(): void $this->analyse([__DIR__ . '/data/scalar-class-name.php'], []); } + public function testBug8473(): void + { + $this->analyse([__DIR__ . '/data/bug-8473.php'], []); + } + + public function testBug11552(): void + { + $this->analyse([__DIR__ . '/data/bug-11552.php'], [ + [ + 'Class Bug11552\SomeResult @extends tag contains unresolvable type.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index e7954fbaa4..27ed122f68 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -3,35 +3,41 @@ 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ClassTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( new TemplateTypeCheck( - $broker, - new ClassCaseSensitivityCheck($broker), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), new GenericObjectTypeCheck(), $typeAliasResolver, - true - ) + true, + ), ); } public function testRule(): void { - require_once __DIR__ . '/data/class-template.php'; - $this->analyse([__DIR__ . '/data/class-template.php'], [ [ 'PHPDoc tag @template for class ClassTemplateType\Foo cannot have existing class stdClass as its name.', @@ -73,6 +79,27 @@ public function testRule(): void 'PHPDoc tag @template for anonymous class cannot have existing type alias TypeAlias as its name.', 78, ], + [ + 'Call-site variance of covariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template U is redundant, template type T of class ClassTemplateType\Consecteur has the same variance.', + 113, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template W is in conflict with covariant template type T of class ClassTemplateType\Consecteur.', + 113, + ], + [ + 'PHPDoc tag @template T for class ClassTemplateType\Elit has invalid default type ClassTemplateType\Zazzzu.', + 121, + ], + [ + 'Default type bool in PHPDoc tag @template T for class ClassTemplateType\Venenatis is not subtype of bound type object.', + 129, + ], + [ + 'PHPDoc tag @template V for class ClassTemplateType\Mauris does not have a default type but follows an optional @template U.', + 139, + ], ]); } @@ -80,26 +107,47 @@ public function testNestedGenericTypes(): void { $this->analyse([__DIR__ . '/data/nested-generic-types.php'], [ [ - 'Type mixed in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of class NestedGenericTypesClassCheck\SomeObjectInterface.', + 'Type mixed in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', 32, ], [ - 'Type int in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of class NestedGenericTypesClassCheck\SomeObjectInterface.', + 'Type int in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', 41, ], [ - 'PHPDoc tag @template U bound contains generic type NestedGenericTypesClassCheck\NotGeneric but class NestedGenericTypesClassCheck\NotGeneric is not generic.', + 'PHPDoc tag @template U bound contains generic type NestedGenericTypesClassCheck\NotGeneric but interface NestedGenericTypesClassCheck\NotGeneric is not generic.', 52, ], [ - 'PHPDoc tag @template V bound has type NestedGenericTypesClassCheck\MultipleGenerics which does not specify all template types of class NestedGenericTypesClassCheck\MultipleGenerics: T, U', + 'PHPDoc tag @template V bound has type NestedGenericTypesClassCheck\MultipleGenerics which does not specify all template types of interface NestedGenericTypesClassCheck\MultipleGenerics: T, U', 52, ], [ - 'PHPDoc tag @template W bound has type NestedGenericTypesClassCheck\MultipleGenerics which specifies 3 template types, but class NestedGenericTypesClassCheck\MultipleGenerics supports only 2: T, U', + 'PHPDoc tag @template W bound has type NestedGenericTypesClassCheck\MultipleGenerics which specifies 3 template types, but interface NestedGenericTypesClassCheck\MultipleGenerics supports only 2: T, U', 52, ], ]); } + public function testBug5446(): void + { + $this->analyse([__DIR__ . '/data/bug-5446.php'], []); + } + + public function testInInterface(): void + { + $this->analyse([__DIR__ . '/data/interface-template.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10049(): void + { + $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..070c8e3021 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -0,0 +1,73 @@ + + */ +class EnumAncestorsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumAncestorsRule( + new GenericAncestorsCheck( + self::createReflectionProvider(), + new GenericObjectTypeCheck(), + new VarianceCheck(), + new UnresolvableTypeHelper(), + [], + true, + ), + new CrossCheckInterfacesHelper(), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testCrossCheckInterfaces(): void + { + $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..6028064d26 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php @@ -0,0 +1,35 @@ + + */ +class EnumTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumTemplateTypeRule(); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/enum-template.php'], [ + [ + 'Enum EnumTemplate\Foo has PHPDoc @template tag but enums cannot be generic.', + 8, + ], + [ + 'Enum EnumTemplate\Bar has PHPDoc @template tags but enums cannot be generic.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php index 015bdb9333..f80773321a 100644 --- a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class FunctionSignatureVarianceRuleTest extends RuleTestCase { @@ -14,7 +14,7 @@ class FunctionSignatureVarianceRuleTest extends RuleTestCase protected function getRule(): Rule { return new FunctionSignatureVarianceRule( - self::getContainer()->getByType(VarianceCheck::class) + self::getContainer()->getByType(VarianceCheck::class), ); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 77b758c6bc..ef504c54da 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -3,24 +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(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -43,6 +56,27 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\resourceBound() with bound type resource is not supported.', 50, ], + [ + 'Call-site variance of covariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template U is redundant, template type T of class FunctionTemplateType\GenericCovariant has the same variance.', + 94, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template W is in conflict with covariant template type T of class FunctionTemplateType\GenericCovariant.', + 94, + ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\invalidDefault() has invalid default type FunctionTemplateType\Zazzzu.', + 102, + ], + [ + 'Default type bool in PHPDoc tag @template T for function FunctionTemplateType\outOfBoundsDefault() is not subtype of bound type object.', + 110, + ], + [ + 'PHPDoc tag @template V for function FunctionTemplateType\requiredAfterOptional() does not have a default type but follows an optional @template U.', + 120, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index 0207076df9..0fe5c98ba1 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,14 +15,15 @@ class InterfaceAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new InterfaceAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true + new UnresolvableTypeHelper(), + [], + true, ), - new CrossCheckInterfacesHelper() + new CrossCheckInterfacesHelper(), ); } @@ -109,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, + ], ]); } @@ -130,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.', @@ -144,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', @@ -189,12 +191,15 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\ExtendsGenericInterface extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 197, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in extended type InterfaceAncestorsExtends\FooGeneric9 of interface InterfaceAncestorsExtends\FooGeneric10.', 215, ], + [ + 'Call-site variance annotation of covariant LogicException in generic type InterfaceAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', + 223, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index e8272578f0..e4c0da47f3 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -3,31 +3,40 @@ 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(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new InterfaceTemplateTypeRule( - self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, 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/interface-template.php'; - $this->analyse([__DIR__ . '/data/interface-template.php'], [ [ 'PHPDoc tag @template for interface InterfaceTemplateType\Foo cannot have existing class stdClass as its name.', @@ -49,7 +58,33 @@ public function testRule(): void 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template U is redundant, template type T of interface InterfaceTemplateType\Covariant has the same variance.', + 74, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', + 74, + ], + [ + 'PHPDoc tag @template T for interface InterfaceTemplateType\InvalidDefault has invalid default type InterfaceTemplateType\Zazzzu.', + 82, + ], + [ + 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', + 90, + ], + [ + 'PHPDoc tag @template V for interface InterfaceTemplateType\RequiredAfterOptional does not have a default type but follows an optional @template U.', + 100, + ], ]); } + public function testInClass(): void + { + $this->analyse([__DIR__ . '/data/class-template.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index 3facb614dd..81392060c2 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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,25 +23,226 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-signature-variance.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 16, ], [ - 'Template type T is declared as covariant, but occurs in invariant position in parameter b of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::c().', + 22, ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-invariant.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-covariant.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Template type W is declared as covariant, but occurs in contravariant position in parameter d of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 'Template type X is declared as covariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter f of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter h of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::c().', + 41, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::e().', + 47, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::f().', + 50, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::h().', + 56, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::j().', + 62, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::k().', + 65, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::l().', + 68, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::m().', + 71, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-contravariant.php'], [ + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\Contravariant\C::a().', 35, ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter d of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter g of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::b().', + 38, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::d().', + 44, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::f().', + 50, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::g().', + 53, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::i().', + 59, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::j().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::k().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::l().', + 68, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::m().', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in param-out type of parameter a of method MethodSignatureVariance\Contravariant\C::paramOut().', + 79, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-constructor.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-static.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', + 49, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\StaticMethod\C::a().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::b().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::d().', + 71, + ], + ]); + } + + public function testBug8880(): void + { + $this->analyse([__DIR__ . '/data/bug-8880.php'], [ + [ + 'Template type T is declared as covariant, but occurs in contravariant position in parameter items of method Bug8880\IProcessor::processItems().', + 17, + ], + ]); + } + + public function testBug9161(): void + { + $this->analyse([__DIR__ . '/data/bug-9161.php'], []); + } + + public function testPr2465(): void + { + $this->analyse([__DIR__ . '/data/pr-2465.php'], [ + [ + 'Template type T is declared as covariant, but occurs in invariant position in parameter thing of method Pr2465\UnitOfTest::foo().', + 16, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10609(): void + { + $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..c936422dce --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php @@ -0,0 +1,64 @@ + + */ +class MethodTagTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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..f26d3e1611 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeTraitRuleTest.php @@ -0,0 +1,65 @@ + + */ +class MethodTagTemplateTypeTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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 d953a401c8..d8b9b5a909 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -3,24 +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 MethodTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -53,6 +66,27 @@ public function testRule(): void 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias ImportedAlias as its name.', 85, ], + [ + 'Call-site variance of covariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class MethodTemplateType\Dolor has the same variance.', + 109, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class MethodTemplateType\Dolor.', + 109, + ], + [ + 'PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::invalid() has invalid default type MethodTemplateType\Zazzzu.', + 122, + ], + [ + 'Default type bool in PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::outOfBounds() is not subtype of bound type object.', + 130, + ], + [ + 'PHPDoc tag @template V for method MethodTemplateType\InvalidDefault::requiredAfterOptional() does not have a default type but follows an optional @template U.', + 140, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php new file mode 100644 index 0000000000..b5aeefa8e9 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php @@ -0,0 +1,140 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testPromoted(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testReadOnly(): void + { + $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'], []); + } + + public function testBug13049(): void + { + $this->analyse([__DIR__ . '/data/bug-13049.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 1b90f838f6..f40914464b 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -3,24 +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 TraitTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -49,6 +62,27 @@ public function testRule(): void 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class TraitTemplateType\Dolor has the same variance.', + 64, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class TraitTemplateType\Dolor.', + 64, + ], + [ + 'PHPDoc tag @template T for trait TraitTemplateType\Adipiscing has invalid default type TraitTemplateType\Zazzzu.', + 72, + ], + [ + 'Default type bool in PHPDoc tag @template T for trait TraitTemplateType\Elit is not subtype of bound type object.', + 80, + ], + [ + 'PHPDoc tag @template V for trait TraitTemplateType\Consecteur does not have a default type but follows an optional @template U.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php index 340d663e34..783d238b53 100644 --- a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -17,11 +18,13 @@ protected function getRule(): Rule return new UsedTraitsRule( self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + new UnresolvableTypeHelper(), + [], + true, + ), ); } @@ -39,7 +42,6 @@ public function testRule(): void [ 'Class UsedTraits\Baz uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 38, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type UsedTraits\GenericTrait in PHPDoc tag @use specifies 2 template types, but trait UsedTraits\GenericTrait supports only 1: T', @@ -52,7 +54,10 @@ public function testRule(): void [ 'Trait UsedTraits\NestedTrait uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 54, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not allowed.', + 69, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/bug-10049.php b/tests/PHPStan/Rules/Generics/data/bug-10049.php new file mode 100644 index 0000000000..e32a2d7399 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-10049.php @@ -0,0 +1,41 @@ += 8.1 + +namespace Bug10049; + +/** + * @template SELF of SimpleEntity + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-10609.php b/tests/PHPStan/Rules/Generics/data/bug-10609.php new file mode 100644 index 0000000000..c62a9e0a10 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-10609.php @@ -0,0 +1,16 @@ + + */ +class SomeResult extends Result { + +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-13049.php b/tests/PHPStan/Rules/Generics/data/bug-13049.php new file mode 100644 index 0000000000..86386bc019 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-13049.php @@ -0,0 +1,41 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13049; + +/** + * @template-covariant Value of string|list + * + * @immutable + */ +final class LanguageProperty +{ + + /** @var Value */ + public $value; + + /** + * @param Value $value + */ + public function __construct($value) + { + $this->value = $value; + } +} + +/** + * @template-covariant Value of string|list + * + * @immutable + */ +final class LanguageProperty2 +{ + /** + * @param Value $value + */ + public function __construct(public $value) + { + $this->value = $value; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-3769.php b/tests/PHPStan/Rules/Generics/data/bug-3769.php index 1101ad3547..aeb273d479 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -29,6 +29,7 @@ function foo( $a = assertType('array', stringValues($foo)); $a = assertType('array', stringValues($bar)); $a = assertType('array', stringValues($baz)); + echo 'test'; }; /** @@ -37,6 +38,7 @@ function foo( */ function fooUnion($foo): void { $a = assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); + echo 'test'; } /** @@ -70,8 +72,8 @@ function stringBound(string $a) } function (): void { - $a = assertType('int', mixedBound(1)); - $a = assertType('string', mixedBound('str')); + $a = assertType('1', mixedBound(1)); + $a = assertType('\'str\'', mixedBound('str')); $a = assertType('1', intBound(1)); $a = assertType('\'str\'', stringBound('str')); }; diff --git a/tests/PHPStan/Rules/Generics/data/bug-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 752983b26a..06400bd536 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -79,3 +79,64 @@ class Dolor { }; + +/** + * @template T of 'string' + */ +class Sit +{ + +} + +/** + * @template T of 5 + */ +class Amet +{ + +} + +/** + * @template-covariant T + */ +class Consecteur +{ + +} + +/** + * @template T of Consecteur + * @template U of Consecteur + * @template V of Consecteur<*> + * @template W of Consecteur + */ +class Adipiscing +{ + +} + +/** + * @template T = Zazzzu + */ +class Elit +{ + +} + +/** + * @template T of object = bool + */ +class Venenatis +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +class Mauris +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/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.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php index cb0977a10c..76cbd59de8 100644 --- a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php @@ -18,7 +18,7 @@ interface ItemListInterface extends \Traversable */ final class ItemList implements \IteratorAggregate, ItemListInterface { - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator([]); } @@ -29,7 +29,7 @@ public function getIterator() */ final class ItemList2 implements \IteratorAggregate, ItemListInterface { - public function getIterator() + 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 11ed760df6..c938c5dff4 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -63,3 +63,61 @@ function nakano() { } + +/** @template T of null */ +function nullSupported() +{ + +} + +/** @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 ed7f3ef667..7f0da436e7 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -58,3 +58,46 @@ interface UnionBound { } + +/** @template-covariant T */ +interface Covariant +{ + +} + +/** + * @template T of Covariant + * @template U of Covariant + * @template V of Covariant<*> + * @template W of Covariant + */ +interface TypeProjections +{ + +} + +/** + * @template T = Zazzzu + */ +interface InvalidDefault +{ + +} + +/** + * @template T of object = bool + */ +interface OutOfBoundsDefault +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +interface RequiredAfterOptional +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php new file mode 100644 index 0000000000..57d4dfc3b9 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php @@ -0,0 +1,72 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php new file mode 100644 index 0000000000..311958192a --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php @@ -0,0 +1,83 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @return X */ + private function n() {} + + /** + * @param-out X $a + */ + public function paramOut(&$a) + { + + } +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php new file mode 100644 index 0000000000..4837dbba5d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php @@ -0,0 +1,75 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @param X $n */ + private function n($n) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php new file mode 100644 index 0000000000..54dd3e4eb7 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php @@ -0,0 +1,70 @@ + $b + * @param In> $c + * @param In> $d + * @param In $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + * @return X + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return In */ + function b() {} + + /** @return In>*/ + function c() {} + + /** @return In>*/ + function d() {} + + /** @return In */ + function e() {} + + /** @return Out */ + function f() {} + + /** @return Out>*/ + function g() {} + + /** @return Out>*/ + function h() {} + + /** @return Out */ + function i() {} + + /** @return Invariant */ + function j() {} + + /** @return Invariant>*/ + function k() {} + + /** @return Invariant>*/ + function l() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php new file mode 100644 index 0000000000..692f6672f2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php @@ -0,0 +1,72 @@ + $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php index c783c69191..e7ffcbfaa2 100644 --- a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php @@ -2,37 +2,22 @@ namespace MethodSignatureVariance; -/** @template-covariant T */ -interface Out { -} - -/** @template T */ -interface Invariant { -} - -/** - * @template-covariant T - * @template-covariant W of \DateTimeInterface - */ class C { /** - * @param Out $a - * @param Invariant $b - * @param T $c - * @param W $d - * @return T + * @template U + * @return void */ - function a($a, $b, $c, $d) { - return $c; - } + function a() {} + /** * @template-covariant U - * @param Out $a - * @param Invariant $b - * @param U $c - * @return U + * @return void + */ + function b() {} + + /** + * @template-contravariant U + * @return void */ - function b($a, $b, $c) { - return $c; - } + function c() {} } diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-template.php new file mode 100644 index 0000000000..77a6f202c2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-template.php @@ -0,0 +1,15 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +class HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php new file mode 100644 index 0000000000..57a93beb5a --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php @@ -0,0 +1,13 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +trait HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index fc6c4c87e2..edf5d62201 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -88,3 +88,58 @@ public function doFoo() } } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +class Sit +{ + + /** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ + public function doSit() + { + + } + +} + +class InvalidDefault +{ + + /** + * @template T = Zazzzu + */ + public function invalid() + { + + } + + /** + * @template T of object = bool + */ + public function outOfBounds() + { + + } + + /** + * @template T + * @template U = string + * @template V + */ + public function requiredAfterOptional() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/pr-2465.php b/tests/PHPStan/Rules/Generics/data/pr-2465.php new file mode 100644 index 0000000000..fe07d78e9d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/pr-2465.php @@ -0,0 +1,17 @@ +> $thing + */ + public function foo(InvariantThing $thing): void {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php new file mode 100644 index 0000000000..fa56e56822 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php @@ -0,0 +1,93 @@ += 8.0 + +namespace PropertyVariance\Promoted; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-covariant X + */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-contravariant X + */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php new file mode 100644 index 0000000000..faefc2bd8b --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php @@ -0,0 +1,89 @@ += 8.1 + +namespace PropertyVariance\ReadOnly; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class D { + /** + * @param X $a + * @param X $b + */ + public function __construct( + public readonly mixed $a, + private readonly mixed $b, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance.php b/tests/PHPStan/Rules/Generics/data/property-variance.php new file mode 100644 index 0000000000..a7c9406b8e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance.php @@ -0,0 +1,102 @@ + */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 8870b4dd98..39126a88f2 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -46,3 +46,48 @@ trait Ipsum { } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +/** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ +trait Sit +{ + +} + +/** + * @template T = Zazzzu + */ +trait Adipiscing +{ + +} + +/** + * @template T of object = bool + */ +trait Elit +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +trait Consecteur +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php index 855d38aa02..f34fb5ffb9 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -61,3 +61,25 @@ class Ipsum use NestedTrait; } + +class Dolor +{ + + /** @use GenericTrait */ + use GenericTrait; + +} + +/** + * @template T = string + */ +trait GenericDefault +{ +} + +class Sit +{ + + use GenericDefault; + +} diff --git a/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php new file mode 100644 index 0000000000..c34760e39e --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php @@ -0,0 +1,55 @@ + + */ +class IgnoreParseErrorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IgnoreParseErrorRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', + 10, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected T_CLOSE_PARENTHESIS after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS', + 13, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected end, unclosed opening parenthesis', + 19, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected T_OTHER \'čičí\' after @phpstan-ignore, expected identifier', + 23, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected end after @phpstan-ignore, expected identifier', + 27, + ], + ]); + } + + public function testRuleWithUnusedTrait(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error-trait.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php new file mode 100644 index 0000000000..176089f480 --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php @@ -0,0 +1,13 @@ + + */ +class RestrictedInternalClassConstantUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedClassConstantUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-constant-internal-tag.php'], [ + [ + 'Access to internal constant ClassConstantInternalTagOne\Foo::INTERNAL from outside its root namespace ClassConstantInternalTagOne.', + 49, + ], + [ + 'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.', + 54, + ], + [ + 'Access to internal constant ClassConstantInternalTagOne\Foo::INTERNAL from outside its root namespace ClassConstantInternalTagOne.', + 62, + ], + + [ + 'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.', + 67, + ], + [ + 'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.', + 89, + ], + [ + 'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.', + 94, + ], + [ + 'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.', + 102, + ], + [ + 'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.', + 107, + ], + ]); + } + + public function testClassConstantAccessOnInternalSubclass(): 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, + ], + ]); + } + + public function testBug12951(): void + { + require_once __DIR__ . '/data/bug-12951-define.php'; + $this->analyse([__DIR__ . '/data/bug-12951-constant.php'], [ + [ + 'Access to constant NUMERIC_COLLATION of internal class Bug12951Polyfill\NumberFormatter from outside its root namespace Bug12951Polyfill.', + 7, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Access to internal constant ClassInternal::INTERNAL_CONSTANT.', + 41, + ], + ]); + } + +} 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..e93244e845 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalMethodUsageExtensionTest.php @@ -0,0 +1,69 @@ + + */ +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, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Call to internal method ClassInternal::internalMethod().', + 38, + ], + ]); + } + +} 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..5fd7b6639a --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticMethodUsageExtensionTest.php @@ -0,0 +1,84 @@ + + */ +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, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Call to internal static method ClassInternal::internalStaticMethod().', + 39, + ], + ]); + } + + public function testBug13210(): void + { + $this->analyse([__DIR__ . '/data/bug-13210.php'], []); + } + +} 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/bug-12951-constant.php b/tests/PHPStan/Rules/InternalTag/data/bug-12951-constant.php new file mode 100644 index 0000000000..ab8ccf7708 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/bug-12951-constant.php @@ -0,0 +1,8 @@ += 8.1 + +namespace Bug12951; + +function (): void { + new \Bug12951Core\NumberFormatter(); + new \Bug12951Polyfill\NumberFormatter(); +}; diff --git a/tests/PHPStan/Rules/InternalTag/data/bug-12951-define.php b/tests/PHPStan/Rules/InternalTag/data/bug-12951-define.php new file mode 100644 index 0000000000..0289ff38af --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/bug-12951-define.php @@ -0,0 +1,34 @@ += 8.1 + +namespace Bug12951; + +function (): void { + \Bug12951Core\NumberFormatter::doBar(); + \Bug12951Polyfill\NumberFormatter::doBar(); + + \Bug12951Core\NumberFormatter::doBar(...); + \Bug12951Polyfill\NumberFormatter::doBar(...); +}; diff --git a/tests/PHPStan/Rules/InternalTag/data/bug-12951-static-property.php b/tests/PHPStan/Rules/InternalTag/data/bug-12951-static-property.php new file mode 100644 index 0000000000..6423ed839c --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/bug-12951-static-property.php @@ -0,0 +1,8 @@ += 8.0 + +/** + * @internal + * + * @readonly + */ +final class DBInternal +{ + public static function phpValueToSql(int|string|BackedEnum|null $phpValue): string + { + return match (true) { + $phpValue === null => 'NULL', + is_string($phpValue) => "''", + is_int($phpValue) => (string)$phpValue, + $phpValue instanceof BackedEnum => self::phpValueToSql($phpValue->value), + }; + } + +} 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/no-namespace.php b/tests/PHPStan/Rules/InternalTag/data/no-namespace.php new file mode 100644 index 0000000000..bad1525561 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/no-namespace.php @@ -0,0 +1,43 @@ +internalMethod(); + self::internalStaticMethod(); + + return self::INTERNAL_CONSTANT; + } +} + +class ClassAccessOnInternal { + protected function getFoo(): string + { + $classInternal = new ClassInternal(); + $classInternal->internalMethod(); + ClassInternal::internalStaticMethod(); + + return ClassInternal::INTERNAL_CONSTANT; + } +} 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 @@ + @@ -10,17 +12,13 @@ class ContinueBreakInLoopRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ContinueBreakInLoopRule(); } public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/continue-break.php'], [ [ 'Keyword break used outside of a loop or a switch statement.', @@ -49,4 +47,31 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $this->analyse([__DIR__ . '/data/continue-break-property-hook.php'], [ + [ + 'Keyword break used outside of a loop or a switch statement.', + 13, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 15, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 24, + ], + [ + 'Keyword continue used outside of a loop or a switch statement.', + 26, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 35, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php new file mode 100644 index 0000000000..7aac5f8019 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php @@ -0,0 +1,107 @@ + + */ +class DeclareStrictTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DeclareStrictTypesRule(new ExprPrinter(new Printer())); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/declare-position.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 5, + ], + ]); + } + + public function testRule2(): void + { + $this->analyse([__DIR__ . '/data/declare-position2.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 1, + ], + ]); + } + + public function testNested(): void + { + $this->analyse([__DIR__ . '/data/declare-position-nested.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 7, + ], + [ + 'Declare strict_types must be the very first statement.', + 12, + ], + ]); + } + + public function testValidPosition(): void + { + $this->analyse([__DIR__ . '/data/declare-position-valid.php'], []); + } + + public function testTicks(): void + { + $this->analyse([__DIR__ . '/data/declare-ticks.php'], []); + } + + public function testMulti(): void + { + $this->analyse([__DIR__ . '/data/declare-multi.php'], []); + } + + public function testShebang(): void + { + $this->analyse([__DIR__ . '/data/declare-shebang.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang2.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang3.php'], []); + } + + public function testHtmlBeforeDecalre(): void + { + $this->analyse([__DIR__ . '/data/declare-inline-html.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 2, + ], + ]); + } + + public function testNonsense(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense.php'], [ + [ + "Declare strict_types must have 0 or 1 as its value, 'foo' given.", + 1, + ], + ]); + } + + public function testNonsenseBool(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense-bool.php'], [ + [ + 'Declare strict_types must have 0 or 1 as its value, \true given.', + 1, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php new file mode 100644 index 0000000000..732819b506 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -0,0 +1,139 @@ + + */ +class RequireFileExistsRuleTest extends RuleTestCase +{ + + private string $currentWorkingDirectory = __DIR__ . '/../'; + + protected function getRule(): Rule + { + return new RequireFileExistsRule($this->currentWorkingDirectory); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../Analyser/usePathConstantsAsConstantString.neon', + ]; + } + + public function testBasicCase(): void + { + $this->analyse([__DIR__ . '/data/require-file-simple-case.php'], [ + [ + 'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 11, + ], + [ + 'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 12, + ], + [ + 'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 13, + ], + [ + 'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 14, + ], + ]); + } + + public function testFileDoesNotExistConditionally(): void + { + $this->analyse([__DIR__ . '/data/require-file-conditionally.php'], [ + [ + 'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 9, + ], + [ + 'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 10, + ], + [ + 'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 11, + ], + [ + 'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 12, + ], + ]); + } + + public function testRelativePath(): void + { + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], [ + [ + 'Path in include() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 8, + ], + [ + 'Path in include_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 9, + ], + [ + 'Path in require() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 10, + ], + [ + 'Path in require_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 11, + ], + ]); + } + + public function testRelativePathWithIncludePath(): void + { + $includePaths = [realpath(__DIR__)]; + $includePaths[] = get_include_path(); + + set_include_path(implode(PATH_SEPARATOR, $includePaths)); + + try { + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); + } finally { + set_include_path($includePaths[1]); + } + } + + public function testRelativePathWithSameWorkingDirectory(): void + { + $this->currentWorkingDirectory = __DIR__; + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); + } + + public function testBug11738(): void + { + $this->analyse([__DIR__ . '/data/bug-11738/bug-11738.php'], []); + } + + public function testBug12203(): void + { + $this->analyse([__DIR__ . '/data/bug-12203.php'], [ + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 5, + ], + [ + 'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 6, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php new file mode 100644 index 0000000000..a4abe2dafc --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php @@ -0,0 +1,2 @@ += 8.4 + +namespace ContinueBreakPropertyHook; + +class Foo +{ + + public int $bar { + set (int $foo) { + foreach ([1, 2, 3] as $val) { + switch ($foo) { + case 1: + break 3; + default: + break 3; + } + } + } + } + + public int $baz { + get { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + } + + public int $ipsum { + get { + foreach ([1, 2, 3] as $val) { + function (): void { + break; + }; + } + } + } + +} + +class ValidUsages +{ + + public int $i { + set (int $foo) { + switch ($foo) { + case 1: + break; + default: + break; + } + + foreach ([1, 2, 3] as $val) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + for ($i = 0; $i < 5; $i++) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + while (true) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + do { + if (rand(0, 1)) { + break; + } else { + continue; + } + } while (true); + } + } + + public int $j { + set (int $foo) { + foreach ([1, 2, 3] as $val) { + switch ($foo) { + case 1: + break 2; + default: + break 2; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php new file mode 100644 index 0000000000..9c270f6a18 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php @@ -0,0 +1,2 @@ +some html + @@ -20,16 +21,13 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->analyse([__DIR__ . '/data/abstract-method.php'], [ [ 'Non-abstract class AbstractMethod\Bar contains abstract method doBar().', 15, ], [ - 'Non-abstract class AbstractMethod\Baz contains abstract method doBar().', + 'Interface AbstractMethod\Baz contains abstract method doBar().', 22, ], ]); @@ -47,7 +45,7 @@ public function testBug3406(): void public function testBug3406ReflectionCheck(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass(ClassFoo::class); $this->assertSame(AbstractFoo::class, $reflection->getNativeMethod('myFoo')->getDeclaringClass()->getName()); $this->assertSame(ClassFoo::class, $reflection->getNativeMethod('myBar')->getDeclaringClass()->getName()); @@ -63,4 +61,56 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11592(): void + { + $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 6fea88f460..354b11b027 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,48 +2,66 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class CallMethodsRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - /** @var bool */ - private $checkNullables; + private bool $checkNullables; - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; - /** @var int */ - private $phpVersion = PHP_VERSION_ID; + private bool $checkImplicitMixed = false; protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($broker, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed); + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), 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), ); } + #[RequiresPhp('< 8.0')] + public function testIsCallablePhp7(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testIsCallablePhp8(): void + { + $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; @@ -390,15 +408,15 @@ public function testCallMethods(): void 914, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'foo\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', 915, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ @@ -406,11 +424,11 @@ public function testCallMethods(): void 942, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -432,14 +450,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.', @@ -475,19 +496,24 @@ public function testCallMethods(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array(\'foo\')|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array 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 string&numeric, 123 given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', 1657, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, \'abc\' given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', 1658, ], [ @@ -503,6 +529,38 @@ public function testCallMethods(): void 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, + ], ]); } @@ -701,23 +759,23 @@ public function testCallMethodsOnThisOnly(): void 867, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'foo\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', 915, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -731,14 +789,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.', @@ -762,19 +823,24 @@ public function testCallMethodsOnThisOnly(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array(\'foo\')|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array 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 string&numeric, 123 given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', 1657, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, \'abc\' given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', 1658, ], [ @@ -790,6 +856,38 @@ public function testCallMethodsOnThisOnly(): void 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, + ], ]); } @@ -863,16 +961,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; @@ -1202,7 +1305,7 @@ public function testCallMethodsNullIssue(): void $this->analyse([__DIR__ . '/data/order.php'], []); } - public function dataIterable(): array + public static function dataIterable(): array { return [ [ @@ -1214,10 +1317,7 @@ public function dataIterable(): array ]; } - /** - * @dataProvider dataIterable - * @param bool $checkNullables - */ + #[DataProvider('dataIterable')] public function testIterables(bool $checkNullables): void { $this->checkThisOnly = false; @@ -1446,7 +1546,7 @@ public function testShadowedTraitMethod(): void $this->analyse([__DIR__ . '/data/shadowed-trait-method.php'], []); } - public function dataExplicitMixed(): array + public static function dataExplicitMixed(): array { return [ [ @@ -1456,6 +1556,10 @@ public function dataExplicitMixed(): array 'Cannot call method foo() on mixed.', 17, ], + [ + 'Cannot call method foo() on T of mixed.', + 26, + ], [ 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 43, @@ -1467,6 +1571,7 @@ public function dataExplicitMixed(): array [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doFoo() expects callable(mixed): void, Closure(int): void given.', 133, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ], [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', @@ -1482,16 +1587,12 @@ public function dataExplicitMixed(): array } /** - * @dataProvider dataExplicitMixed - * @param bool $checkExplicitMixed - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataExplicitMixed')] public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1499,6 +1600,46 @@ public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], $errors); } + public static function dataImplicitMixed(): array + { + return [ + [ + true, + [ + [ + 'Cannot call method foo() on mixed.', + 16, + ], + [ + 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', + 42, + ], + [ + 'Parameter #1 $cb of method CheckImplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', + 139, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataImplicitMixed')] + public function testImplicitMixed(bool $checkImplicitMixed, array $errors): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/check-implicit-mixed.php'], $errors); + } + public function testBug3409(): void { $this->checkThisOnly = false; @@ -1538,11 +1679,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; @@ -1573,9 +1709,6 @@ public function testBug3481(): void public function testBug3683(): void { - if (self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires hybrid reflection.'); - } $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1587,12 +1720,9 @@ public function testBug3683(): void ]); } + #[RequiresPhp('>= 8.0')] 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; @@ -1614,10 +1744,6 @@ public function testStringableStrictTypes(): void public function testMatchExpressionVoidIsUsed(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1635,10 +1761,6 @@ public function testMatchExpressionVoidIsUsed(): void public function testNullSafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1656,18 +1778,20 @@ public function testNullSafe(): void 'Parameter #1 $passedByRef of method NullsafeMethodCall\Foo::doBaz() is passed by reference, so it expects variables only.', 27, ], + [ + 'Cannot call method foo() on null.', + 33, + ], + [ + 'Cannot call method foo() on null.', + 34, + ], ]); } + #[RequiresPhp('< 8.0')] public function testDisallowNamedArguments(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP earlier than 8.0.'); - } - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1680,16 +1804,26 @@ public function testDisallowNamedArguments(): void ]); } - public function testNamedArguments(): void + public function testDisallowNamedArgumentsInPhpVersionScope(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedArguments(): void + { $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/named-arguments.php'], [ [ @@ -1769,7 +1903,7 @@ public function testNamedArguments(): void 91, ], [ - 'Parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', 91, ], [ @@ -1784,15 +1918,19 @@ public function testNamedArguments(): void 'Unpacked argument (...) cannot be followed by a non-unpacked argument.', 94, ], + [ + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], + [ + 'Named argument bar for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], ]); } public function testBug4199(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1807,10 +1945,6 @@ public function testBug4199(): void public function testBug4188(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1825,7 +1959,7 @@ public function testOnlyRelevantUnableToResolveTemplateType(): void $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/only-relevant-unable-to-resolve-template-type.php'], [ [ - 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', + 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', 41, ], [ @@ -1889,7 +2023,7 @@ public function testBug4557(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4557.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4557.php'], []); } public function testBug4209(): void @@ -1897,7 +2031,7 @@ public function testBug4209(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209.php'], []); } public function testBug4209Two(): void @@ -1905,7 +2039,7 @@ public function testBug4209Two(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209-2.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209-2.php'], []); } public function testBug3321(): void @@ -1913,7 +2047,7 @@ public function testBug3321(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3321.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3321.php'], []); } public function testBug4498(): void @@ -1921,7 +2055,7 @@ public function testBug4498(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4498.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4498.php'], []); } public function testBug3922(): void @@ -1929,7 +2063,7 @@ public function testBug3922(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3922.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3922.php'], [ [ 'Parameter #1 $query of method Bug3922\FooQueryHandler::handle() expects Bug3922\FooQuery, Bug3922\BarQuery given.', 63, @@ -1942,7 +2076,7 @@ public function testBug4642(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4642.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4642.php'], []); } public function testBug4008(): void @@ -1961,16 +2095,13 @@ public function testBug3546(): void $this->analyse([__DIR__ . '/data/bug-3546.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug4800(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; + $this->analyse([__DIR__ . '/data/bug-4800.php'], [ [ 'Missing parameter $bar (string) in call to method Bug4800\HelloWorld2::a().', @@ -2006,10 +2137,6 @@ public function testUnableToResolveCallbackParameterType(): void public function testBug4083(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2040,15 +2167,24 @@ public function testBug5258(): void $this->analyse([__DIR__ . '/data/bug-5258.php'], []); } + public function testBug5591(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5591.php'], []); + } + public function testGenericObjectLowerBound(): void { $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/generic-object-lower-bound.php'], [ + $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', ], ]); } @@ -2076,22 +2212,35 @@ public function testBug5536(): void public function testBug5372(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-5372.php'], [ + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 64, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 68, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 72, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 81, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], - /*[ + [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 85, - ],*/ + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], ]); } @@ -2122,11 +2271,11 @@ public function testLiteralString(): void 58, ], [ - 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', 60, ], [ - 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', 65, ], [ @@ -2193,4 +2342,1337 @@ public function testBug3465(): void $this->analyse([__DIR__ . '/data/bug-3465.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug5868(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6239(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testReadOnlyPropertyPassedByReference(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6118(): void + { + $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', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6904(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6291(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7593(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug8058(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-8058.php'], []); + } + + #[RequiresPhp('< 8.2')] + public function testBug8058b(): void + { + $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 static 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]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataCallablesWithoutCheckNullables')] + 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); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8713(): void + { + $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.\n• Exception does not have property \$bar." : "• Exception might not have property \$foo.\n• Exception might not have property \$bar.", + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', + 15, + "• Exception might not have property \$foo.\n• Exception might not have property \$bar.", + ], + [ + '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, + 'object{foo?: string} might not have property $foo.', + ], + [ + '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.', + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9951(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9951.php'], [ + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, mixed given.', + 26, + ], + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, object|string|null given.', + 31, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testTypedClassConstants(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/return-type-class-constant.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testNamedParametersForMultiVariantFunctions(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBugTemplateMixedUnionIntersect(): void + { + $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 testBug9487(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-9487.php'], [ + [ + 'Parameter #1 $x of method Bug9487\HelloWorld::sayHello() expects list, array, string> given.', + 15, + 'array, string> is not a list.', + ], + ]); + } + + 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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testNoNamedArguments(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12422(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13171(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-13171.php'], [ + [ + 'Parameter #1 $value of method Fiber::resume() expects void, int given.', + 9, + ], + ]); + } + + public function testBug10719(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-10719.php'], []); + } + + public function testBug9141(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9141.php'], []); + } + + public function testBug12548(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12548.php'], []); + } + + public function testBug3589(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-3589.php'], [ + [ + 'Parameter #1 $fooId of method Bug3589\FooRepository::load() expects Bug3589\Id, Bug3589\Id given.', + 35, + ], + [ + 'Parameter #1 $fooId of method Bug3589\FooRepository::load() expects Bug3589\Id, Bug3589\Id given.', + 41, + ], + ]); + } + + public function testBug5642(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-5642.php'], [ + [ + 'Method Couchbase\BucketManager::flush() invoked with 0 parameters, 1 required.', + 9, + ], + [ + 'Method Couchbase\BucketManager::flush() invoked with 2 parameters, 1 required.', + 11, + ], + ]); + } + + public function testBug3396(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-3396.php'], []); + } + + public function testBug13511(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13511.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php index d96da60225..25cd02b49b 100644 --- a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** @@ -10,7 +11,7 @@ class CallPrivateMethodThroughStaticRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new CallPrivateMethodThroughStaticRule(); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index d0c05ddd38..c2e095528c 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,41 +2,66 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use function usort; /** - * @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 = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallStaticMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), 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'], [ [ @@ -226,7 +251,7 @@ public function testCallStaticMethods(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 'Call to an undefined static method CallStaticMethods\CallWithStatic::nonexistent().', 344, ], ]); @@ -387,12 +412,9 @@ public function testBug2164(): void ]); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/static-method-named-arguments.php'], [ @@ -428,12 +450,33 @@ public function testBug4550(): void ]); } + #[RequiresPhp('< 8.0')] public function testBug1971(): void { $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ - 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array(class-string, \'sayHello2\') given.', + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug1971Php8(): void + { + $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, ], ]); @@ -457,4 +500,420 @@ public function testBug4886(): void $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 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 static 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, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/call-static-method-mixed.php'], $errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBugWrongMethodNameWithTemplateMixed(): void + { + $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 testBug13267(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-13267.php'], []); + } + + 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, + ], + ]); + } + + public function testBug13556(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-13556.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php index 917cc77f8b..29d1f4f52a 100644 --- a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php @@ -13,7 +13,7 @@ class CallToConstructorStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToConstructorStatementWithoutSideEffectsRule(self::createReflectionProvider()); } public function testRule(): void @@ -23,6 +23,14 @@ public function testRule(): void 'Call to Exception::__construct() on a separate line has no effect.', 6, ], + [ + 'Call to new PDOStatement() on a separate line has no effect.', + 11, + ], + [ + 'Call to new stdClass() on a separate line has no effect.', + 12, + ], [ 'Call to ConstructorStatementNoSideEffects\ConstructorWithPure::__construct() on a separate line has no effect.', 57, @@ -31,6 +39,10 @@ public function testRule(): void 'Call to ConstructorStatementNoSideEffects\ConstructorWithPureAndThrowsVoid::__construct() on a separate line has no effect.', 58, ], + [ + 'Call to new ConstructorStatementNoSideEffects\NoConstructor() on a separate line has no effect.', + 68, + ], ]); } @@ -39,4 +51,9 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455-constructor.php'], []); } + public function testBug12224(): void + { + $this->analyse([__DIR__ . '/data/bug-12224.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..fb3e55dee4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php @@ -0,0 +1,38 @@ + + */ +class CallToMethodStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithNoDiscardRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-call-statement-result-discarded.php'], [ + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', + 20, + ], + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', + 21, + ], + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::differentCase() on a separate line discards return value.', + 30, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index a2a63eb692..d52edeb11a 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -5,19 +5,46 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } + #[RequiresPhp('>= 8.0')] 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 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, + ], + ]); + } + + #[RequiresPhp('< 8')] + public function testRulePhp7(): void { $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects.php'], [ [ @@ -45,10 +72,6 @@ public function testRule(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-method-call-statement-no-side-effects.php'], [ [ 'Call to method Exception::getMessage() on a separate line has no effect.', @@ -67,15 +90,23 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure5() on a separate line has no effect.', + 59, ], ]); } @@ -85,4 +116,47 @@ 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 testBug12224(): void + { + $this->analyse([__DIR__ . '/data/bug-12224.php'], []); + } + + 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/CallToStaticMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..5c320def63 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php @@ -0,0 +1,38 @@ + + */ +class CallToStaticMethodStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new CallToStaticMethodStatementWithNoDiscardRule( + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + $reflectionProvider, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-method-call-statement-result-discarded.php'], [ + [ + 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::staticMethod() on a separate line discards return value.', + 19, + ], + [ + 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::differentCase() on a separate line discards return value.', + 27, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index 08832088ee..95afe389cd 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -5,32 +5,42 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class CallToStaticMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $broker = self::createReflectionProvider(); return new CallToStaticMethodStatementWithoutSideEffectsRule( - new RuleLevelHelper($broker, true, false, true, false), - $broker + new RuleLevelHelper($broker, true, false, true, false, false, false, true), + $broker, ); } + #[RequiresPhp('>= 8.0')] 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, ], + ]); + } + + #[RequiresPhp('< 8')] + public function testRulePhp7(): 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.', - 13, + 12, ], [ 'Call to method DateTime::format() on a separate line has no effect.', @@ -44,19 +54,27 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure5() on a separate line has no effect.', + 59, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\PureThrows::pureAndThrowsVoid() on a separate line has no effect.', - 67, + 85, ], ]); } @@ -66,4 +84,41 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455-static.php'], []); } + public function testBug12224(): void + { + $this->analyse([__DIR__ . '/data/bug-12224.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, + ], + ]); + } + + public function testBug10819(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Call to static method DateTime::createFromFormat() on a separate line has no effect.', + 13, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-10819.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorDeclarationRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorDeclarationRuleTest.php new file mode 100644 index 0000000000..15448d9530 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorDeclarationRuleTest.php @@ -0,0 +1,29 @@ + + */ +class ConsistentConstructorDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ConsistentConstructorDeclarationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/consistent-constructor-declaration.php'], [ + [ + 'Private constructor cannot be enforced as consistent for child classes.', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php new file mode 100644 index 0000000000..8b4c5fd973 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php @@ -0,0 +1,64 @@ + */ +class ConsistentConstructorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ConsistentConstructorRule( + new ConsistentConstructorHelper(), + 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, + ], + [ + 'Parameter #1 $i of method ConsistentConstructor\ChildTwo::__construct() is not optional.', + 102, + ], + ]); + } + + 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 70be8fb673..ee7e28cb76 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -4,29 +4,46 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = self::createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } $this->analyse([__DIR__ . '/data/typehints.php'], [ [ 'Method TestMethodTypehints\FooMethodTypehints::foo() has invalid return type TestMethodTypehints\NonexistentClass.', @@ -88,10 +105,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, @@ -143,9 +168,6 @@ public function testExistingClassInIterableTypehint(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of method VoidParameterTypehintMethod\Foo::doFoo() has invalid type void.', @@ -154,12 +176,8 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - return []; - } - return [ [ 70400, @@ -182,38 +200,70 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataNativeUnionTypes')] public function testNativeUnionTypes(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } - public function dataRequiredParameterAfterOptional(): array + public static function dataRequiredParameterAfterOptional(): array { return [ [ 70400, - PHP_VERSION_ID < 80000 || self::$useStaticReflectionProvider ? [] : [ + [ + [ + "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, + ], [ - 'Required parameter $bar follows optional parameter $foo', + "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, ], [ - 'Required parameter $bar follows optional parameter $foo', + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 17, ], [ - 'Required parameter $bar follows optional parameter $foo', + '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, + ], ], ], [ - 80000, + 80100, [ [ 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', @@ -227,16 +277,93 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 21, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.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 int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataRequiredParameterAfterOptional')] public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; @@ -253,4 +380,262 @@ public function testBug4641(): void ]); } + public static 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, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataIntersectionTypes')] + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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); + } + + #[RequiresPhp('>= 8.0')] + public function testConditionalReturnType(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type T of method ParamOutTemplate\FooBar::uselessLocalTemplate() is not referenced in a parameter.', + 22, + ], + ]); + } + + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testDeprecatedImplicitlyNullableParameterType(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12501(): void + { + $this->analyse([__DIR__ . '/data/bug-12501.php'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::nothing().', + 8, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::alsoNothing().', + 12, + ], + [ + 'Attribute NoDiscard cannot be used on never method TestMethodTypehints\Demo::returnNever().', + 16, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__construct().', + 19, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__destruct().', + 25, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__unset().', + 31, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__wakeup().', + 37, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__clone().', + 43, + ], + ]); + } + } 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..e9def238c3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -0,0 +1,76 @@ + */ +class FinalPrivateMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateMethodRule(); + } + + public static function dataRule(): array + { + return [ + [ + 70400, + [], + ], + [ + 80000, + [ + [ + 'Private method FinalPrivateMethod\Foo::foo() cannot be final as it is never overridden by other classes.', + 8, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataRule')] + 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 cf9640685c..df7e897d77 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class IncompatibleDefaultParameterTypeRuleTest extends RuleTestCase { @@ -45,4 +46,38 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10956(): void + { + $this->analyse([__DIR__ . '/data/bug-10956.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 7bae918088..2245a8cb2a 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,31 +22,33 @@ class MethodAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new MethodAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/method-attributes.php'], [ [ 'Attribute class MethodAttributes\Foo does not have the method target.', @@ -53,4 +57,14 @@ public function testRule(): void ]); } + public function testBug5898(): void + { + $this->analyse([__DIR__ . '/data/bug-5898.php'], []); + } + + public function testDeprecatedAttribute(): void + { + $this->analyse([__DIR__ . '/data/deprecated-attribute.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php new file mode 100644 index 0000000000..6dcf45e242 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -0,0 +1,82 @@ + + */ +class MethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = self::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), + ); + } + + #[RequiresPhp('< 8.1')] + public function testNotSupportedOnOlderVersions(): void + { + $this->analyse([__DIR__ . '/data/method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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 536dff3540..b23da34309 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -3,26 +3,36 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; 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(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), + false, ); } @@ -71,7 +81,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 +126,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 +155,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 +184,7 @@ public function testReturnTypeRuleWithoutMaybes(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClass::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 358, ], - ] + ], ); } @@ -210,17 +220,22 @@ public function testBug3997(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-3997.php'], [ [ - 'Return type (string) of method Bug3997\Ipsum::count() should be compatible with return type (int) of method Countable::count()', - 59, + 'Return type (int) of method Bug3997\Baz::count() should be covariant with return type (int<0, max>) of method Countable::count()', + 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 { - if (PHP_VERSION_ID < 70200 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.2 or later.'); - } $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4003.php'], [ @@ -229,7 +244,7 @@ public function testBug4003(): void 15, ], [ - PHP_VERSION_ID < 70200 ? 'Parameter #1 $test (mixed) of method Bug4003\Ipsum::doFoo() does not match parameter #1 $test (int) of method Bug4003\Lorem::doFoo().' : 'Parameter #1 $test (string) of method Bug4003\Ipsum::doFoo() should be compatible with parameter $test (int) of method Bug4003\Lorem::doFoo()', + 'Parameter #1 $test (string) of method Bug4003\Ipsum::doFoo() should be compatible with parameter $test (int) of method Bug4003\Lorem::doFoo()', 38, ], ]); @@ -237,9 +252,6 @@ public function testBug4003(): void public function testBug4017(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4017.php'], []); @@ -308,7 +320,7 @@ public function testBug4707(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707.php'], [ [ - 'Return type (array) of method Bug4707\Block2::getChildren() should be compatible with return type (array>) of method Bug4707\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707\Block2::getChildren() should be compatible with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', 38, ], ]); @@ -320,7 +332,7 @@ public function testBug4707Covariant(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707-covariant.php'], [ [ - 'Return type (array) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (array>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (list>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', 38, ], ]); @@ -342,10 +354,6 @@ public function testBug4729(): void public function testBug4854(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4854.php'], []); @@ -353,13 +361,201 @@ public function testBug4854(): void public function testMemcachePoolGet(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/memcache-pool-get.php'], []); } + public function testOverridenMethodWithConditionalReturnType(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/overriden-method-with-conditional-return-type.php'], [ + [ + 'Return type (($p is int ? stdClass : string)) of method OverridenMethodWithConditionalReturnType\Bar2::doFoo() should be compatible with return type (($p is int ? int : string)) of method OverridenMethodWithConditionalReturnType\Foo::doFoo()', + 37, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7652(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testTraits(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10166(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10184(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testOverridenAbstractTraitMethodPhpDoc(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10240(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10240.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10488(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10488.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12073(): void + { + $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..ad3c23c992 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php @@ -0,0 +1,39 @@ + + */ +class MissingMagicSerializationMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingMagicSerializationMethodsRule(new PhpVersion(PHP_VERSION_ID)); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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 bdd6304f97..ff51b465e8 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 PHPUnit\Framework\Attributes\RequiresPhp; /** * @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,7 +29,7 @@ public function testRule(): void 24, ], [ - 'Non-abstract class class@anonymous/tests/PHPStan/Rules/Methods/data/missing-method-impl.php:41 contains abstract method doFoo() from interface MissingMethodImpl\Foo.', + 'Non-abstract class MissingMethodImpl\Foo@anonymous/tests/PHPStan/Rules/Methods/data/missing-method-impl.php:41 contains abstract method doFoo() from interface MissingMethodImpl\Foo.', 41, ], ]); @@ -48,4 +45,20 @@ public function testBug3958(): void $this->analyse([__DIR__ . '/data/bug-3958.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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 f8ec513525..fce941c3ce 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -3,17 +3,18 @@ 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, true, [])); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -42,32 +43,49 @@ public function testRule(): void [ 'Method MissingMethodParameterTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 58, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericInterface() has parameter $i with generic interface MissingMethodParameterTypehint\GenericInterface but does not specify its types: T, U', 91, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericClass() has parameter $c with generic class MissingMethodParameterTypehint\GenericClass but does not specify its types: A, B', 101, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection2() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 119, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.', 180, ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 207, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 215, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamClosureThisType::generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 226, + ], + [ + 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', + 238, + ], + [ + 'Method MissingMethodParameterTypehint\Baz::acceptsGenericWithSomeDefaults() has parameter $c with generic class MissingMethodParameterTypehint\GenericClassWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 270, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); @@ -75,19 +93,16 @@ public function testRule(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/missing-typehint-promoted-properties.php'], [ [ 'Method MissingTypehintPromotedProperties\Foo::__construct() has parameter $foo with no value type specified in iterable type array.', 8, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingTypehintPromotedProperties\Bar::__construct() has parameter $foo with no value type specified in iterable type array.', 21, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); } @@ -98,12 +113,11 @@ public function testDeepInspectTypes(): void [ 'Method DeepInspectTypes\Foo::doFoo() has parameter $foo with no value type specified in iterable type iterable.', 11, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method DeepInspectTypes\Foo::doBar() has parameter $bars with generic class DeepInspectTypes\Bar but does not specify its types: T', 17, - MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP, ], ]); } @@ -113,4 +127,25 @@ 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 2216c37618..ffeb6ce61b 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingMethodReturnTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingMethodReturnTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -38,22 +40,24 @@ public function testRule(): void [ 'Method MissingMethodReturnTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 46, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericInterface() return type with generic interface MissingMethodReturnTypehint\GenericInterface does not specify its types: T, U', 79, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericClass() return type with generic class MissingMethodReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\CallableSignature::doFoo() return type has no signature specified for callable.', 99, ], + [ + 'Method MissingMethodReturnTypehint\Baz::returnsGenericWithSomeDefaults() return type with generic class MissingMethodReturnTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 142, + ], ]); } @@ -64,7 +68,7 @@ public function testIndirectInheritanceBug2740(): void public function testArrayTypehintWithoutNullInPhpDoc(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/array-typehint-without-null-in-phpdoc.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/array-typehint-without-null-in-phpdoc.php'], []); } public function testBug4415(): void @@ -82,4 +86,41 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9657(): void + { + $this->analyse([__DIR__ . '/data/bug-9657.php'], []); + } + + public function testInheritPhpDocReturnTypeWithNarrowerNativeReturnType(): void + { + $this->analyse([__DIR__ . '/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php new file mode 100644 index 0000000000..cc74d519e9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php @@ -0,0 +1,39 @@ + + */ +class MissingMethodSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new MissingMethodSelfOutTypeRule(new MissingTypehintCheck(true, [])); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-method-self-out-type.php'], [ + [ + 'Method MissingMethodSelfOutType\Foo::doFoo() has PHPDoc tag @phpstan-self-out with no value type specified in iterable type array.', + 14, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Method MissingMethodSelfOutType\Foo::doFoo2() has PHPDoc tag @phpstan-self-out with generic class MissingMethodSelfOutType\Foo but does not specify its types: T', + 22, + ], + [ + 'Method MissingMethodSelfOutType\Foo::doFoo3() has PHPDoc tag @phpstan-self-out with no signature specified for callable.', + 30, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php index 55fb808a0d..ff7b48186a 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -18,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-method-call-rule.php'], [ [ 'Using nullsafe method call on non-nullable type Exception. Use -> instead.', @@ -30,4 +27,41 @@ public function testRule(): void ]); } + public function testNullsafeVsScalar(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/nullsafe-vs-scalar.php'], []); + } + + public function testBug8664(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8664.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9293(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9293.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6922b(): void + { + $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 939caa2cd4..cb2171e112 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -3,8 +3,13 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_filter; +use function array_values; use const PHP_VERSION_ID; /** @@ -13,19 +18,28 @@ 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(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), + $this->checkMissingOverrideMethodAttribute, ); } - public function dataOverridingFinalMethod(): array + public static function dataOverridingFinalMethod(): array { return [ [ @@ -41,17 +55,9 @@ public function dataOverridingFinalMethod(): array ]; } - /** - * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - * @param string $contravariantMessage - */ + #[DataProvider('dataOverridingFinalMethod')] public function testOverridingFinalMethod(int $phpVersion, string $contravariantMessage): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $errors = [ [ 'Method OverridingFinalMethod\Bar::doFoo() overrides final method OverridingFinalMethod\Foo::doFoo().', @@ -86,7 +92,7 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 115, ], [ - 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', + 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', 125, ], [ @@ -122,22 +128,24 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 280, ], [ - 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $offset (mixed) of method ArrayAccess::offsetExists().', + 'Method OverridingFinalMethod\ExtendsFinalWithAnnotation::doFoo() overrides @final method OverridingFinalMethod\FinalWithAnnotation::doFoo().', + 303, + ], + [ + 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $index (mixed) of method SplFixedArray::offsetExists().', 313, ], ]; if (PHP_VERSION_ID >= 80000) { - $errors = array_values(array_filter($errors, static function (array $error): bool { - return $error[1] !== 125; - })); + $errors = array_values(array_filter($errors, static fn (array $error): bool => $error[1] !== 125)); } $this->phpVersionId = $phpVersion; $this->analyse([__DIR__ . '/data/overriding-method.php'], $errors); } - public function dataParameterContravariance(): array + public static function dataParameterContravariance(): array { return [ [ @@ -220,26 +228,21 @@ public function dataParameterContravariance(): array } /** - * @dataProvider dataParameterContravariance - * @param string $file - * @param int $phpVersion - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataParameterContravariance')] public function testParameterContravariance( string $file, int $phpVersion, - array $expectedErrors + array $expectedErrors, ): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersion; $this->analyse([$file], $expectedErrors); } - public function dataReturnTypeCovariance(): array + public static function dataReturnTypeCovariance(): array { return [ [ @@ -284,35 +287,21 @@ public function dataReturnTypeCovariance(): array } /** - * @dataProvider dataReturnTypeCovariance - * @param int $phpVersion - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ + #[DataProvider('dataReturnTypeCovariance')] 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 - */ + #[DataProvider('dataOverridingFinalMethod')] 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'], [ [ @@ -332,10 +321,7 @@ public function testVariadicParameterIsAlwaysOptional(): void $this->analyse([__DIR__ . '/data/variadic-always-optional.php'], []); } - /** - * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - */ + #[DataProvider('dataOverridingFinalMethod')] public function testBug3403(int $phpVersion): void { $this->phpVersionId = $phpVersion; @@ -356,29 +342,14 @@ public function testBug3478(): void public function testBug3629(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test require static reflection.'); - } $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-3629.php'], []); } public function testVariadics(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = PHP_VERSION_ID; - $errors = []; - if (PHP_VERSION_ID < 70200) { - $errors[] = [ - 'Parameter #2 $lang (mixed) of method OverridingVariadics\Translator::translate() does not match parameter #2 $parameters (string) of method OverridingVariadics\ITranslator::translate().', - 24, - ]; - } - - $errors = array_merge($errors, [ + $errors = [ [ 'Parameter #2 $lang of method OverridingVariadics\OtherTranslator::translate() is not optional.', 34, @@ -395,19 +366,12 @@ public function testVariadics(): void 'Parameter #2 $lang of method OverridingVariadics\YetAnotherTranslator::translate() is not variadic.', 54, ], - ]); - - if (PHP_VERSION_ID < 70200) { - $errors[] = [ - 'Parameter #2 $lang (mixed) of method OverridingVariadics\YetAnotherTranslator::translate() does not match parameter #2 $parameters (string) of method OverridingVariadics\ITranslator::translate().', - 54, - ]; - } + ]; $this->analyse([__DIR__ . '/data/overriding-variadics.php'], $errors); } - public function dataLessOverridenParametersWithVariadic(): array + public static function dataLessOverridenParametersWithVariadic(): array { return [ [ @@ -464,20 +428,16 @@ public function dataLessOverridenParametersWithVariadic(): array } /** - * @dataProvider dataLessOverridenParametersWithVariadic - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataLessOverridenParametersWithVariadic')] public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/less-parameters-variadics.php'], $errors); } - public function dataParameterTypeWidening(): array + public static function dataParameterTypeWidening(): array { return [ [ @@ -497,15 +457,11 @@ public function dataParameterTypeWidening(): array } /** - * @dataProvider dataParameterTypeWidening - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataParameterTypeWidening')] public function testParameterTypeWidening(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/parameter-type-widening.php'], $errors); } @@ -516,4 +472,360 @@ public function testBug4516(): void $this->analyse([__DIR__ . '/data/bug-4516.php'], []); } + public static 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.', + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataTentativeReturnTypes')] + 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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBugWithIndirectPrototype(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9615(): void + { + $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); + } + + #[RequiresPhp('>= 8.3')] + public function testOverrideAttribute(): void + { + $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 static 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, + ], + ]]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataCheckMissingOverrideAttribute')] + 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); + } + + #[RequiresPhp('>= 8.3')] + public function testBug12471(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testSimpleXmlElementChildClass(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/simple-xml-element-child.php'], []); + } + + #[RequiresPhp('>= 8.3')] + public function testFixOverride(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->checkMissingOverrideMethodAttribute = true; + $this->fix(__DIR__ . '/data/fix-override-attribute.php', __DIR__ . '/data/fix-override-attribute.php.fixed'); + } + + #[RequiresPhp('>= 8.3')] + public function testFixWithTabs(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->checkMissingOverrideMethodAttribute = true; + $this->fix(__DIR__ . '/data/fix-with-tabs.php', __DIR__ . '/data/fix-with-tabs.php.fixed'); + } + } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 13fe986ae1..dd51c3df3a 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -3,20 +3,28 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkUnionTypes = true; + + private bool $checkBenevolentUnionTypes = false; + + protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, $this->checkBenevolentUnionTypes, true))); } public function testReturnTypeRule(): void @@ -180,103 +188,123 @@ public function testReturnTypeRule(): void ], [ 'Method ReturnTypes\ReturnTernary::returnTernary() should return ReturnTypes\Foo but returns false.', - 625, + 627, ], [ 'Method ReturnTypes\TrickyVoid::returnVoidOrInt() should return int|void but returns string.', - 656, + 658, ], [ 'Method ReturnTypes\TernaryWithJsonEncode::toJson() should return string but returns string|false.', - 687, + 689, ], [ 'Method ReturnTypes\AppendedArrayReturnType::foo() should return array but returns array.', - 700, + 702, ], [ 'Method ReturnTypes\AppendedArrayReturnType::bar() should return array but returns array.', - 710, + 712, ], [ 'Method ReturnTypes\WrongMagicMethods::__toString() should return string but returns true.', - 720, + 722, ], [ 'Method ReturnTypes\WrongMagicMethods::__isset() should return bool but returns int.', - 725, + 727, ], [ 'Method ReturnTypes\WrongMagicMethods::__destruct() with return type void returns int but should not return anything.', - 730, + 732, ], [ 'Method ReturnTypes\WrongMagicMethods::__unset() with return type void returns int but should not return anything.', - 735, + 737, ], [ 'Method ReturnTypes\WrongMagicMethods::__sleep() should return array but returns array.', - 740, + 742, ], [ 'Method ReturnTypes\WrongMagicMethods::__wakeup() with return type void returns int but should not return anything.', - 747, + 749, ], [ 'Method ReturnTypes\WrongMagicMethods::__set_state() should return object but returns array.', - 752, + 754, ], [ 'Method ReturnTypes\WrongMagicMethods::__clone() with return type void returns int but should not return anything.', - 757, + 759, ], [ - 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', - 815, + '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.', - 1238, + 1240, ], [ 'Method ReturnTypes\NeverReturn::doBaz3() should never return but return statement found.', - 1251, + 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); + } + + #[RequiresPhp('>= 8.0')] public function testMisleadingTypehintsInClassWithoutNamespace(): void { $this->analyse([__DIR__ . '/data/misleadingTypehints.php'], [ @@ -358,9 +386,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.', @@ -373,24 +398,24 @@ public function testBug3997(): void { $this->analyse([__DIR__ . '/data/bug-3997.php'], [ [ - 'Method Bug3997\Foo::count() should return int but returns string.', - 12, + "Method Bug3997\Foo::count() should return int<0, max> but returns 'foo'.", + 13, ], [ - 'Method Bug3997\Bar::count() should return int but returns string.', - 22, + "Method Bug3997\Bar::count() should return int<0, max> but returns 'foo'.", + 24, ], [ 'Method Bug3997\Baz::count() should return int but returns string.', - 35, + 38, ], [ 'Method Bug3997\Lorem::count() should return int but returns string.', - 48, + 52, ], [ 'Method Bug3997\Dolor::count() should return int<0, max> but returns -1.', - 72, + 78, ], ]); } @@ -411,6 +436,7 @@ public function testBug3117(): void [ 'Method Bug3117\SimpleTemporal::adjustInto() should return T of Bug3117\Temporal but returns $this(Bug3117\SimpleTemporal).', 35, + 'Type $this(Bug3117\SimpleTemporal) is not always the same as T. It breaks the contract for some argument types, typically subtypes.', ], ]); } @@ -420,6 +446,11 @@ public function testBug3034(): void $this->analyse([__DIR__ . '/data/bug-3034.php'], []); } + public function testBug3951(): void + { + $this->analyse([__DIR__ . '/data/bug-3951.php'], []); + } + public function testInferArrayKey(): void { $this->analyse([__DIR__ . '/data/infer-array-key.php'], []); @@ -429,16 +460,24 @@ public function testBug4590(): void { $this->analyse([__DIR__ . '/data/bug-4590.php'], [ [ - 'Method Bug4590\Controller::test1() should return Bug4590\OkResponse> but returns Bug4590\OkResponse string)>.', - 39, + '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::test2() should return Bug4590\OkResponse> but returns Bug4590\OkResponse.', + '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::test3() should return Bug4590\OkResponse> but returns Bug4590\OkResponse.', + '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', ], ]); } @@ -490,15 +529,17 @@ public function testBug3118(): void public function testBug4795(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } $this->analyse([__DIR__ . '/data/bug-4795.php'], []); } public function testBug4803(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4803.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4803.php'], []); + } + + public function testBug7020(): void + { + $this->analyse([__DIR__ . '/data/bug-7020.php'], []); } public function testBug2573(): void @@ -506,11 +547,9 @@ public function testBug2573(): void $this->analyse([__DIR__ . '/data/bug-2573-return.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug4603(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } $this->analyse([__DIR__ . '/data/bug-4603.php'], []); } @@ -526,15 +565,10 @@ public function testTemplateUnion(): void 'Method ReturnTemplateUnion\Foo::doFoo2() should return T of bool|float|int|string but returns (T of bool|float|int|string)|null.', 25, ], - [ - // should not be reported - 'Method ReturnTemplateUnion\Foo::doFoo3() should return (T of bool|float|int|string)|null but returns (T of bool|float|int|string)|null.', - 35, - ], ]); } - public function dataBug5218(): array + public static function dataBug5218(): array { return [ [ @@ -554,14 +588,692 @@ public function dataBug5218(): array } /** - * @dataProvider dataBug5218 - * @param bool $checkExplicitMixed - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataBug5218')] 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.", + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7904(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8071(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8071.php'], [ + [ + // there should be no errors + 'Method Bug8071\Inheritance::inherit() should return array but returns array.', + 17, + 'Type string is not always the same as TValues. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug3499(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3499.php'], []); + } + + public function testBug8174(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8174.php'], [ + [ + "Method Bug8174\HelloWorld::filterList() should return list but returns array, '23423'>.", + 21, + "array, '23423'> might not be a list.", + ], + ]); + } + + public function testBug7519(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7519.php'], []); + } + + public function testBug8223(): void + { + $this->checkBenevolentUnionTypes = true; + + $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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testShortGetPropertyHook(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug1O580(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12927(): void + { + $this->analyse([__DIR__ . '/data/bug-12927.php'], []); + } + + public function testBug4443(): void + { + $this->analyse([__DIR__ . '/data/bug-4443.php'], [ + [ + 'Method Bug4443\HelloWorld::getArray() should return array but returns array|null.', + 22, + ], + ]); + } + + public function testBug13043(): void + { + $this->analyse([__DIR__ . '/data/bug-13043.php'], []); + } + + public function testBug12739(): void + { + $this->analyse([__DIR__ . '/data/bug-12739.php'], []); + } + + public function testBug12928(): void + { + $this->analyse([__DIR__ . '/data/bug-12928.php'], [ + [ + 'Method Bug12928\FooBarBaz::render() should return non-empty-string but returns string.', + 59, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7225(): void + { + $this->analyse([__DIR__ . '/data/bug-7225.php'], []); + } + + public function testDeepDimFetch(): void + { + $this->analyse([__DIR__ . '/data/deep-dim-fetch.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9494(): void + { + $this->analyse([__DIR__ . '/data/bug-9494.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php new file mode 100644 index 0000000000..2511b87083 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -0,0 +1,116 @@ + + */ +class StaticMethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = self::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), + ); + } + + #[RequiresPhp('< 8.1')] + public function testNotSupportedOnOlderVersions(): void + { + $this->analyse([__DIR__ . '/data/static-method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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/array-cast-list-types.php b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php new file mode 100644 index 0000000000..6e0c308721 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php @@ -0,0 +1,29 @@ + $var + */ + public function foo($var): void {} + + /** + * @param literal-string $literalString + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param numeric-string $numericString + * @param resource $resource + */ + public function bar(string $literalString, string $nonEmptyString, string $nonFalsyString, string $numericString, $resource) { + $this->foo((array) true); + $this->foo((array) $literalString); + $this->foo((array) 1.0); + $this->foo((array) 1); + $this->foo((array) $resource); + $this->foo((array) (fn () => 'closure')); + $this->foo((array) $nonEmptyString); + $this->foo((array) $nonFalsyString); + $this->foo((array) $numericString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php new file mode 100644 index 0000000000..d892ab794d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php @@ -0,0 +1,83 @@ + $a + * @return list + */ + public function doFoo(array $a): array + { + array_push($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_push($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_push($b, ...$a); + + return $b; + } + +} + +class Bar +{ + + /** + * @param list $a + * @return list + */ + public function doFoo(array $a): array + { + array_unshift($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_unshift($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_unshift($b, ...$a); + + return $b; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php b/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php index 449d409b2c..a71fa7d809 100644 --- a/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php +++ b/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php @@ -1,4 +1,4 @@ -= 7.4 + $this->returnVoid(); + } + + public function returnVoid(): void + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10043.php b/tests/PHPStan/Rules/Methods/data/bug-10043.php new file mode 100644 index 0000000000..9980c932ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10043.php @@ -0,0 +1,18 @@ +{$name}; + } +} + +class StdSat extends \stdClass +{ + use WarnDynamicPropertyTrait; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10153.php b/tests/PHPStan/Rules/Methods/data/bug-10153.php new file mode 100644 index 0000000000..e2b5f06840 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10153.php @@ -0,0 +1,28 @@ +someMethod()->methodFromChild(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-10165.php b/tests/PHPStan/Rules/Methods/data/bug-10165.php new file mode 100644 index 0000000000..c414f64533 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10165.php @@ -0,0 +1,13 @@ + */ + abstract public function foo(): Collection; +} + +class Baz +{ + /** @use FooTrait */ + use FooTrait; + + /** @return Collection */ + public function foo(): Collection + { + /** @var Collection */ + return new Collection(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10208.php b/tests/PHPStan/Rules/Methods/data/bug-10208.php new file mode 100644 index 0000000000..abc33152ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10208.php @@ -0,0 +1,24 @@ +key = null; + + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10240.php b/tests/PHPStan/Rules/Methods/data/bug-10240.php new file mode 100644 index 0000000000..60047d13a6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10240.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug10240; + +interface MyInterface +{ + /** + * @phpstan-param truthy-string $truthyStrParam + */ + public function doStuff( + string $truthyStrParam, + ): void; +} + +trait MyTrait +{ + /** + * @phpstan-param truthy-string $truthyStrParam + */ + abstract public function doStuff( + string $truthyStrParam, + ): void; +} + +class MyClass implements MyInterface +{ + use MyTrait; + + public function doStuff( + string $truthyStrParam, + ): void + { + // ... + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10291.php b/tests/PHPStan/Rules/Methods/data/bug-10291.php new file mode 100644 index 0000000000..cb23fe92cf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10291.php @@ -0,0 +1,25 @@ +myrand(); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10488.php b/tests/PHPStan/Rules/Methods/data/bug-10488.php new file mode 100644 index 0000000000..fec3d30001 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10488.php @@ -0,0 +1,17 @@ += 8.0 + +namespace Bug10488; + +trait Bar +{ + /** + * @param array $data + */ + + abstract protected function test(array $data): void; +} + +abstract class Foo +{ + use Bar; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10580.php b/tests/PHPStan/Rules/Methods/data/bug-10580.php new file mode 100644 index 0000000000..b9c479000a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10580.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10580; + +interface FooI { + /** @return $this */ + public function fooThisInterface(): FooI; + /** @return $this */ + public function fooThisClass(): FooI; + /** @return $this */ + public function fooThisSelf(): self; + /** @return $this */ + public function fooThisStatic(): static; +} + +final class FooA implements FooI +{ + public function fooThisInterface(): FooI { return new FooA(); } + public function fooThisClass(): FooA { return new FooA(); } + public function fooThisSelf(): self { return new FooA(); } + public function fooThisStatic(): static { return new FooA(); } +} + +final class FooB implements FooI +{ + /** @return $this */ + public function fooThisInterface(): FooI { return new FooB(); } + /** @return $this */ + public function fooThisClass(): FooB { return new FooB(); } + /** @return $this */ + public function fooThisSelf(): self { return new FooB(); } + /** @return $this */ + public function fooThisStatic(): static { return new FooB(); } + /** @return $this */ + public function fooThis(): static { return new FooB(); } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10653.php b/tests/PHPStan/Rules/Methods/data/bug-10653.php new file mode 100644 index 0000000000..3aaa3c735b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10653.php @@ -0,0 +1,42 @@ +throwOnFailure($this->mayFail()); + } + + /** + * @template T + * + * @param T $result + * @return (T is false ? never : T) + */ + public function throwOnFailure($result) + { + if ($result === false) { + throw new Exception('Operation failed'); + } + return $result; + } + + /** + * @return stdClass|false + */ + public function mayFail() + { + $this->Counter++; + return $this->Counter % 2 ? new stdClass() : false; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10715.php b/tests/PHPStan/Rules/Methods/data/bug-10715.php new file mode 100644 index 0000000000..82cd7f66ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10715.php @@ -0,0 +1,30 @@ + $word + * + * @return ($word is array ? array : string) + */ + public static function wgtrim(string|array $word): string|array + { + if (\is_array($word)) { + return array_map(static::wgtrim(...), $word); + } + + return 'word'; + } + + /** + * @param array{foo: array, bar: string} $array + * + * @return array{foo: array, bar: string} + */ + public static function example(array $array): array + { + return array_map(static::wgtrim(...), $array); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10719.php b/tests/PHPStan/Rules/Methods/data/bug-10719.php new file mode 100644 index 0000000000..abe94e40cf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10719.php @@ -0,0 +1,16 @@ +setTimestamp($dt2->getTimestamp() + 1000); +} + +if ($dt1 > $dt2) { + echo $dt1->getTimestamp(); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10819.php b/tests/PHPStan/Rules/Methods/data/bug-10819.php new file mode 100644 index 0000000000..5f79b0827f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10819.php @@ -0,0 +1,20 @@ + + */ + 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-12224.php b/tests/PHPStan/Rules/Methods/data/bug-12224.php new file mode 100644 index 0000000000..42ee7864cc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12224.php @@ -0,0 +1,50 @@ +string($a); +new AssertConstructor($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-12548.php b/tests/PHPStan/Rules/Methods/data/bug-12548.php new file mode 100644 index 0000000000..2ea2544bac --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12548.php @@ -0,0 +1,86 @@ +createStdSat(); + $o->foo(); // @phpstan-ignore method.nonObject (EXPECTED) + + $o = $this->createStdSat(); + if (StdSat::assertInstanceOf($o)) { + $o->foo(); + } + + $o = $this->createStdSat(); + StdSat::assertInstanceOf2($o); + $o->foo(); + + $o = $this->createStdSat(); + StdSat::assertInstanceOf3($o); + $o->foo(); + } +} 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 @@ +> + */ + abstract public static function getClassIdentify() : string; +} + +/** + * @extends ScalarType + */ +class MyInt extends ScalarType { + public static function getClassIdentify() : string { + return MyInt::class; + } +} + +/** + * @extends ScalarType + */ +class MyString extends ScalarType { + public static function getClassIdentify() : string { + return MyString::class; + } +} + +/** + * @extends ScalarType + */ +class MyLowerString extends ScalarType { + public static function getClassIdentify() : string { + return MyLowerString::class; + } +} + +/** + * @extends ScalarType + */ +class MyUpperString extends ScalarType { + public static function getClassIdentify() : string { + return MyUpperString::class; + } +} + +/** + * @extends ScalarType + */ +class MyClassString extends ScalarType { + public static function getClassIdentify() : string { + return MyClassString::class; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12793.php b/tests/PHPStan/Rules/Methods/data/bug-12793.php new file mode 100644 index 0000000000..63339ebe09 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12793.php @@ -0,0 +1,26 @@ +{$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-12927.php b/tests/PHPStan/Rules/Methods/data/bug-12927.php new file mode 100644 index 0000000000..0331446aec --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12927.php @@ -0,0 +1,63 @@ + $list + * @return list> + */ + public function sayHello(array $list): array + { + foreach($list as $k => $v) { + unset($list[$k]['abc']); + assertType('non-empty-list', $list); + assertType('array{}|array{abc: string}', $list[$k]); + } + return $list; + } + + /** + * @param list> $list + */ + public function sayFoo(array $list): void + { + foreach($list as $k => $v) { + unset($list[$k]['abc']); + assertType('non-empty-list>', $list); + assertType('array', $list[$k]); + } + assertType('list>', $list); + } + + /** + * @param list> $list + */ + public function sayFoo2(array $list): void + { + foreach($list as $k => $v) { + $list[$k]['abc'] = 'world'; + assertType("non-empty-list&hasOffsetValue('abc', 'world')>", $list); + assertType("non-empty-array&hasOffsetValue('abc', 'world')", $list[$k]); + } + assertType("list&hasOffsetValue('abc', 'world')>", $list); + } + + /** + * @param list> $list + */ + public function sayFooBar(array $list): void + { + foreach($list as $k => $v) { + if (rand(0,1)) { + unset($list[$k]); + } + assertType('array, array>', $list); + assertType('array', $list[$k]); + } + assertType('array', $list[$k]); + } +} 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-13043.php b/tests/PHPStan/Rules/Methods/data/bug-13043.php new file mode 100644 index 0000000000..405cb75383 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13043.php @@ -0,0 +1,24 @@ +prefix; + + $search = str_split('()<>@'); + $replace = array_map(rawurlencode(...), $search); + + $name .= str_replace($search, $replace, $this->name); + + return $name; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-13171.php b/tests/PHPStan/Rules/Methods/data/bug-13171.php new file mode 100644 index 0000000000..8765a9f028 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13171.php @@ -0,0 +1,10 @@ + $a */ +function foo (Fiber $a): void { + $a->resume(1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-13267.php b/tests/PHPStan/Rules/Methods/data/bug-13267.php new file mode 100644 index 0000000000..f49948300e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13267.php @@ -0,0 +1,16 @@ +getMessage(), + $e->getCode(), + 1, + $e->getFile(), + $e->getLine(), + $e->getPrevious(), + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-13511.php b/tests/PHPStan/Rules/Methods/data/bug-13511.php new file mode 100644 index 0000000000..cf7efde5be --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13511.php @@ -0,0 +1,24 @@ +printInfo($user); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-13556.php b/tests/PHPStan/Rules/Methods/data/bug-13556.php new file mode 100644 index 0000000000..6b3d371989 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13556.php @@ -0,0 +1,13 @@ + true; +}; + +$gen2 = static function () { + yield false => false; +}; + +$ait = new AppendIterator(); + +$ait->append($gen1()); +$ait->append($gen2()); diff --git a/tests/PHPStan/Rules/Methods/data/bug-1953.php b/tests/PHPStan/Rules/Methods/data/bug-1953.php new file mode 100644 index 0000000000..c70978996c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1953.php @@ -0,0 +1,13 @@ +bar(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3034.php b/tests/PHPStan/Rules/Methods/data/bug-3034.php index a83ebe47a5..f693787c09 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3034.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3034.php @@ -15,7 +15,7 @@ class HelloWorld implements \IteratorAggregate /** * @return \ArrayIterator */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->list); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-3396.php b/tests/PHPStan/Rules/Methods/data/bug-3396.php new file mode 100644 index 0000000000..17670e97b5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3396.php @@ -0,0 +1,20 @@ +takesString(stream_get_contents($stream)); + $this->takesString(stream_get_contents($stream, 1)); + $this->takesString(stream_get_contents($stream, 1, 1)); + } +} 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-3478.php b/tests/PHPStan/Rules/Methods/data/bug-3478.php index 68b6993ec5..0bfd2f9209 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3478.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3478.php @@ -4,6 +4,7 @@ class ExtendedDocument extends \DOMDocument { + #[\ReturnTypeWillChange] public function saveHTML(\DOMNode $node = null) { return parent::saveHTML($node); diff --git a/tests/PHPStan/Rules/Methods/data/bug-3499.php b/tests/PHPStan/Rules/Methods/data/bug-3499.php new file mode 100644 index 0000000000..2aff67f920 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3499.php @@ -0,0 +1,21 @@ +foo->getClone(); + } +} + + diff --git a/tests/PHPStan/Rules/Methods/data/bug-3589.php b/tests/PHPStan/Rules/Methods/data/bug-3589.php new file mode 100644 index 0000000000..f82e1bbcdd --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3589.php @@ -0,0 +1,41 @@ + $fooId + */ + public function load(Id $fooId): Foo + { + // ... + return new Foo; + } +} + +$fooRepository = new FooRepository; + +// Expected behavior: no error +/** @var Id */ +$fooId = new Id; +$fooRepository->load($fooId); + +// Expected behavior: error on line 33 +/** @var Id */ +$barId = new Id; +$fooRepository->load($barId); + +// Expected behavior: errors +// - line 38 - Template Tpl is not specified +// - line 39 - Parameter #1 fooId of method FooRepository::load() expects Id, nonspecified Id given. +$unknownId = new Id; +$fooRepository->load($unknownId); 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-3997.php b/tests/PHPStan/Rules/Methods/data/bug-3997.php index cbf3fda6ee..66278ec527 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3997.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3997.php @@ -7,6 +7,7 @@ class Foo implements Countable { + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -17,6 +18,7 @@ public function count() class Bar implements Countable { + #[\ReturnTypeWillChange] public function count(): int { return 'foo'; @@ -30,6 +32,7 @@ class Baz implements Countable /** * @return int */ + #[\ReturnTypeWillChange] public function count(): int { return 'foo'; @@ -43,6 +46,7 @@ class Lorem implements Countable /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -56,6 +60,7 @@ class Ipsum implements Countable /** * @return string */ + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -67,6 +72,7 @@ class Dolor implements Countable { /** @return positive-int|0 */ + #[\ReturnTypeWillChange] public function count(): int { return -1; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4008.php b/tests/PHPStan/Rules/Methods/data/bug-4008.php index 7d93147d5f..68414bb3cb 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4008.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4008.php @@ -29,3 +29,12 @@ class OtherGenericClass{} abstract class BaseModel{} class Model extends BaseModel{} + +/** + * @template T of Model + * @extends GenericClass + */ +class ChildGenericGenericClass extends GenericClass +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4083.php b/tests/PHPStan/Rules/Methods/data/bug-4083.php index c3a6141896..42bf6d0393 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4083.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4083.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class GenericList implements IteratorAggregate +{ + /** @var array */ + protected $items = []; + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + /** + * @return ?T + */ + public function broken(int $key) + { + $item = $this->items[$key] ?? null; + if ($item) { + } + + return $item; + } + + /** + * @return ?T + */ + public function works(int $key) + { + $item = $this->items[$key] ?? null; + + return $item; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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 index 6b0744876c..8181dbc4a4 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4188.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4188.php @@ -1,4 +1,4 @@ -= 7.4 +value = $value; + } + + final public static function fromString(string $value): self + { + return new static($value); + } +} + +final class ClassB extends ClassC +{ +} + +final class ClassA +{ + public function classB(): ClassB + { + return ClassB::fromString("any"); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4443.php b/tests/PHPStan/Rules/Methods/data/bug-4443.php new file mode 100644 index 0000000000..9f7ff6a28e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4443.php @@ -0,0 +1,26 @@ + */ + private static ?array $arr = null; + + private static function setup(): void + { + self::$arr = null; + } + + /** @return array */ + public static function getArray(): array + { + if (self::$arr === null) { + self::$arr = []; + self::setup(); + } + return self::$arr; + } +} + +HelloWorld::getArray(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-4590.php b/tests/PHPStan/Rules/Methods/data/bug-4590.php index a3db4a3445..ed2d79d790 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4590.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4590.php @@ -27,6 +27,14 @@ public function getBody() { return $this->body; } + + /** + * @return static> + */ + public static function testGenericStatic() + { + return new static(["ok" => "hello"]); + } } class Controller diff --git a/tests/PHPStan/Rules/Methods/data/bug-4758.php b/tests/PHPStan/Rules/Methods/data/bug-4758.php new file mode 100644 index 0000000000..e45329dd1f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4758.php @@ -0,0 +1,26 @@ + + */ + public function doStuff(): array + { + return [[]]; + } +} + +trait TraitTwo +{ + use TraitOne { + TraitOne::doStuff as doStuffFromTraitOne; + } +} + +class SomeController +{ + use TraitTwo; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php new file mode 100644 index 0000000000..a09d113367 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -0,0 +1,25 @@ + + */ + public function work(?callable $a): I; +} + +/** + * @param I $i + */ +function x(I $i) { + assertType('Bug4801\\I', $i->work(null)); + assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4854.php b/tests/PHPStan/Rules/Methods/data/bug-4854.php index 82ff7c0eb2..17a590c626 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4854.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4854.php @@ -23,7 +23,7 @@ abstract class AbstractDomainsAvailability implements DomainsAvailabilityInterfa /** * {@inheritdoc} */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->domains); } @@ -43,7 +43,7 @@ public function offsetSet($offset, $value): void /** * {@inheritdoc} */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return isset($this->domains[$offset]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5008.php b/tests/PHPStan/Rules/Methods/data/bug-5008.php new file mode 100644 index 0000000000..f14d3aef2b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5008.php @@ -0,0 +1,14 @@ + $b; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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 index e3578f2235..eb5306ece8 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5089.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5089.php @@ -16,6 +16,6 @@ public function encode(string $foo): array public function test(): void { - assertType('*NEVER*', $this->encode('foo')); + assertType('never', $this->encode('foo')); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5232.php b/tests/PHPStan/Rules/Methods/data/bug-5232.php new file mode 100644 index 0000000000..4089988ff7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5232.php @@ -0,0 +1,26 @@ += 7.4 +', $col); $newCol = $col->map(static fn(string $var): string => $var . 'bar'); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map(static fn(string $var): string => $classString); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map2(static fn(string $var): string => $classString); @@ -77,11 +77,11 @@ public function doBar(string $literalString) { $col = new Collection(['foo', 'bar']); $newCol = $col->map(static fn(string $var): string => $literalString); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map2(static fn(string $var): string => $literalString); - assertType('Bug5372\Collection', $newCol); // should be literal-string + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5436.php b/tests/PHPStan/Rules/Methods/data/bug-5436.php index 0cbba7cffa..a79fa83492 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5436.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5436.php @@ -4,6 +4,7 @@ final class PDO extends \PDO { + #[\ReturnTypeWillChange] public function query(string $query, ?int $fetchMode = null, ...$fetchModeArgs) { return parent::query($query, $fetchMode, ...$fetchModeArgs); diff --git a/tests/PHPStan/Rules/Methods/data/bug-5460.php b/tests/PHPStan/Rules/Methods/data/bug-5460.php new file mode 100644 index 0000000000..d4c041649f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5460.php @@ -0,0 +1,15 @@ +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-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-5642.php b/tests/PHPStan/Rules/Methods/data/bug-5642.php new file mode 100644 index 0000000000..f5063c6ffe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5642.php @@ -0,0 +1,13 @@ +flush(); + $manager->flush(''); + $manager->flush('', ''); + } +} 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-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-7225.php b/tests/PHPStan/Rules/Methods/data/bug-7225.php new file mode 100644 index 0000000000..4de9017ff9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7225.php @@ -0,0 +1,18 @@ += 8.0 + +namespace Bug7225; + +use DateTimeImmutable; + +class CustomDateTimeImmutable extends DateTimeImmutable +{ + public function test(): self + { + return CustomDateTimeImmutable::createFromInterface(new DateTime()); + } + + public function fromFormat(): CustomDateTimeImmutable|false + { + return CustomDateTimeImmutable::createFromFormat('H:i:s', '00:00:00'); + } +} 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 @@ +startTime = new DateTimeImmutable(); + } + + public function getStartTime(): ?DateTimeImmutable + { + return $this->startTime; + } + +} + +$helloWorld = new HelloWorld(); +if ($helloWorld->getStartTime() > new DateTimeImmutable()) { + echo sprintf('%s', $helloWorld->getStartTime()->format('d.m.y.')); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9391.php b/tests/PHPStan/Rules/Methods/data/bug-9391.php new file mode 100644 index 0000000000..9fd87c5816 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9391.php @@ -0,0 +1,50 @@ + + */ + 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-9487.php b/tests/PHPStan/Rules/Methods/data/bug-9487.php new file mode 100644 index 0000000000..6bcf34c49d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9487.php @@ -0,0 +1,17 @@ + $x */ + public function sayHello($x): void + { + } + + /** @param array $x */ + public function invoke($x): void + { + $this->sayHello($x); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9494.php b/tests/PHPStan/Rules/Methods/data/bug-9494.php new file mode 100644 index 0000000000..3e9146ab6b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9494.php @@ -0,0 +1,53 @@ + 0-indexed memoization */ + protected $mem = []; + + public function __construct(public int $limit) { + $this->mem = array_fill(2, $limit, null); + } + + /** + * Calculate fib, 1-indexed + */ + public function fib(int $n): int + { + if ($n < 1 || $n > $this->limit) { + throw new \RangeException(); + } + + if ($n == 1 || $n == 2) { + return 1; + } + + if (is_null($this->mem[$n - 1])) { + $this->mem[$n - 1] = $this->fib($n - 1) + $this->fib($n - 2); + } + + return $this->mem[$n - 1]; // Is always an int at this stage + } + + /** + * Calculate fib, 0-indexed + */ + public function fib0(int $n0): int + { + if ($n0 < 0 || $n0 >= $this->limit) { + throw new \RangeException(); + } + + if ($n0 == 0 || $n0 == 1) { + return 1; + } + + if (is_null($this->mem[$n0])) { + $this->mem[$n0] = $this->fib0($n0 - 1) + $this->fib0($n0 - 2); + } + + return $this->mem[$n0]; + } +} 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 f8838371a5..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'); } @@ -1752,3 +1752,106 @@ public function doBar() } } + +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-static-method-mixed.php b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php new file mode 100644 index 0000000000..4dcb333ed1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php @@ -0,0 +1,171 @@ += 8.0 + +namespace CallStaticMethodMixed; + +class Foo +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $implicit::foo(); + $explicit::foo(); + } + + /** + * @template T + * @param T $t + */ + public function doBar($t): void + { + $t::foo(); + } + +} + +class Bar +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + self::doBar($implicit); + self::doBar($explicit); + + self::acceptImplicitMixed($implicit); + self::acceptImplicitMixed($explicit); + + self::acceptExplicitMixed($implicit); + self::acceptExplicitMixed($explicit); + + self::acceptVariadicArguments(...$implicit); + self::acceptVariadicArguments(...$explicit); + } + + public static function doBar(int $i): void + { + + } + + public static function acceptImplicitMixed($mixed): void + { + + } + + public static function acceptExplicitMixed(mixed $mixed): void + { + + } + + public static function acceptVariadicArguments(mixed... $args): void + { + + } + + /** + * @template T + * @param T $t + */ + public function doLorem($t): void + { + self::doBar($t); + self::acceptImplicitMixed($t); + self::acceptExplicitMixed($t); + self::acceptVariadicArguments(...$t); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public static function callAcceptsExplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public static function callAcceptsInt(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public static function callReturnsExplicitMixed(callable $cb): void + { + + } + + public static function callReturnsImplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public static function callReturnsInt(callable $cb): void + { + + } + + public static function doLorem(int $i, mixed $explicitMixed, $implicitMixed): void + { + $acceptsInt = function (int $i): void { + + }; + self::callAcceptsExplicitMixed($acceptsInt); + self::callAcceptsInt($acceptsInt); + + $acceptsExplicitMixed = function (mixed $m): void { + + }; + self::callAcceptsExplicitMixed($acceptsExplicitMixed); + self::callAcceptsInt($acceptsExplicitMixed); + + $acceptsImplicitMixed = function ($m): void { + + }; + self::callAcceptsExplicitMixed($acceptsImplicitMixed); + self::callAcceptsInt($acceptsImplicitMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + self::callReturnsExplicitMixed($returnsInt); + self::callReturnsImplicitMixed($returnsInt); + self::callReturnsInt($returnsInt); + + $returnsExplicitMixed = function () use ($explicitMixed): mixed { + return $explicitMixed; + }; + self::callReturnsExplicitMixed($returnsExplicitMixed); + self::callReturnsImplicitMixed($returnsExplicitMixed); + self::callReturnsInt($returnsExplicitMixed); + + $returnsImplicitMixed = function () use ($implicitMixed): mixed { + return $implicitMixed; + }; + self::callReturnsExplicitMixed($returnsImplicitMixed); + self::callReturnsImplicitMixed($returnsImplicitMixed); + self::callReturnsInt($returnsImplicitMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/call-static-methods.php b/tests/PHPStan/Rules/Methods/data/call-static-methods.php index ddcf7e2c2d..0fadae62c3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-static-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-static-methods.php @@ -345,3 +345,11 @@ public function doBar() } } + +class Bug2759 { + public function sayHello(string $html): void + { + $dom = \DOMDocument::loadHTML($html, LIBXML_NOWARNING | LIBXML_NONET | LIBXML_NOERROR); + } +} + diff --git a/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php new file mode 100644 index 0000000000..e4d1cc923c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php @@ -0,0 +1,89 @@ +doBar(function (?float $f): ?float { + return $f; + }); + $this->doBaz(function (?float $f): ?float { + return $f; + }); + $this->doBar(function (?float $f): float { + return $f; + }); + $this->doBaz(function (?float $f): float { + return $f; + }); + + $this->doBar(function (float $f): float { + return $f; + }); + $this->doBaz(function (float $f): float { + return $f; + }); + + $this->doBar2(function (?float $f): ?float { + return $f; + }); + $this->doBaz2(function (?float $f): ?float { + return $f; + }); + $this->doBar2(function (?float $f): float { + return $f; + }); + $this->doBaz2(function (?float $f): float { + return $f; + }); + + $this->doBar2(function (float $f): float { + return $f; + }); + $this->doBaz2(function (float $f): float { + return $f; + }); + } + + /** + * @param callable(float|null): (float|null) $cb + * @return void + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param Closure(float|null): (float|null) $cb + * @return void + */ + public function doBaz(Closure $cb): void + { + + } + + /** + * @param callable(float|null): float $cb + * @return void + */ + public function doBar2(callable $cb): void + { + + } + + /** + * @param Closure(float|null): float $cb + * @return void + */ + public function doBaz2(Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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-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-declaration.php b/tests/PHPStan/Rules/Methods/data/consistent-constructor-declaration.php new file mode 100644 index 0000000000..eebd6e7657 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/consistent-constructor-declaration.php @@ -0,0 +1,36 @@ +getNameScopeKey($fileName, $className, $traitName, $functionName); + if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits + throw new \RuntimeException(); + } + + if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency + throw new \RuntimeException(); + } + + if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { + $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; + $this->inProcess[$fileName][$nameScopeKey] = true; + $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); + } + + return $this->inProcess[$fileName][$nameScopeKey]; + } + + private function getNameScopeKey( + ?string $file, + ?string $class, + ?string $trait, + ?string $function, + ): string + { + return ''; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/default-value-for-promoted-property.php b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php similarity index 76% rename from tests/PHPStan/Rules/Properties/data/default-value-for-promoted-property.php rename to tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php index 649b7b2e56..a2c881155b 100644 --- a/tests/PHPStan/Rules/Properties/data/default-value-for-promoted-property.php +++ b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php @@ -8,7 +8,8 @@ class Foo public function __construct( private int $foo = 'foo', /** @var int */ private $foo = '', - private int $baz = 1 + private int $baz = 1, + private int $intProp = null, ) {} } diff --git a/tests/PHPStan/Rules/Methods/data/deprecated-attribute.php b/tests/PHPStan/Rules/Methods/data/deprecated-attribute.php new file mode 100644 index 0000000000..a3ab23b27f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/deprecated-attribute.php @@ -0,0 +1,13 @@ += 80000) { + class Foo + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} else { + class FooBar + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/discussion-7004.php b/tests/PHPStan/Rules/Methods/data/discussion-7004.php new file mode 100644 index 0000000000..145ed7f694 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/discussion-7004.php @@ -0,0 +1,50 @@ + $data + */ + public static function fromArray1(array $data): void + { + assertType('array', $data); + } + + /** + * @param array{array{newsletterName: string, subscriberCount: int}} $data + */ + public static function fromArray2(array $data): void + { + assertType('array{array{newsletterName: string, subscriberCount: int}}', $data); + } + + /** + * @param array{newsletterName: string, subscriberCount: int} $data + */ + public static function fromArray3(array $data): void + { + assertType('array{newsletterName: string, subscriberCount: int}', $data); + } +} + +class Bar +{ + /** + * @param mixed $data + */ + public function doSomething($data): void + { + if (!is_array($data)) { + return; + } + + assertType('array', $data); + Foo::fromArray1($data); + Foo::fromArray2($data); + Foo::fromArray3($data); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-call.php b/tests/PHPStan/Rules/Methods/data/dynamic-call.php new file mode 100644 index 0000000000..3a917c0c81 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-call.php @@ -0,0 +1,62 @@ +$foo(); + echo $this->$string(); + echo $this->$obj(); + echo $this->{self::$name}(); + } + + public function testStaticCall(string $string, object $obj): void + { + $foo = 'bar'; + + echo self::$foo(); + echo self::$string(); + echo self::$obj(); + echo self::{self::$name}(); + } + + public function testScope(): void + { + $param1 = 1; + $param2 = 'str'; + $name1 = 'doFoo'; + if (rand(0, 1)) { + $name = $name1; + $param = $param1; + } else { + $name = 'doQux'; + $param = $param2; + } + + $this->$name($param); // ok + $this->$name1($param); + $this->$name($param1); + $this->$name($param2); + + self::$name($param); // ok + self::$name1($param); + self::$name($param1); + self::$name($param2); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/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/fix-override-attribute.php b/tests/PHPStan/Rules/Methods/data/fix-override-attribute.php new file mode 100644 index 0000000000..974d6598ee --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/fix-override-attribute.php @@ -0,0 +1,34 @@ += 8.3 + +namespace FixOverrideAttribute; + +class Foo +{ + + public function doFoo(): void + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): void + { + + } + + + public function doBar(): void + { + + } + + #[\Override] + public function doBaz(): void + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/fix-override-attribute.php.fixed b/tests/PHPStan/Rules/Methods/data/fix-override-attribute.php.fixed new file mode 100644 index 0000000000..749dad7abe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/fix-override-attribute.php.fixed @@ -0,0 +1,34 @@ += 8.3 + +namespace FixOverrideAttribute; + +class Foo +{ + + public function doFoo(): void + { + + } + +} + +class Bar extends Foo +{ + + #[\Override] + public function doFoo(): void + { + + } + + + public function doBar(): void + { + + } + + public function doBaz(): void + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php b/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php new file mode 100644 index 0000000000..1aa2d68f1a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php @@ -0,0 +1,28 @@ + */ + public function foo(): Collection; +} + +interface BarI +{ + +} +class Bar implements BarI {} + +/** @template-coveriant TValue */ +class Collection {} + + +class Baz implements FooInterface +{ + /** @return Collection */ + public function foo(): Collection + { + /** @var Collection */ + return new Collection(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php.fixed b/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php.fixed new file mode 100644 index 0000000000..1d46bb3a2b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/fix-with-tabs.php.fixed @@ -0,0 +1,29 @@ + */ + public function foo(): Collection; +} + +interface BarI +{ + +} +class Bar implements BarI {} + +/** @template-coveriant TValue */ +class Collection {} + + +class Baz implements FooInterface +{ + /** @return Collection */ + #[\Override] + public function foo(): Collection + { + /** @var Collection */ + return new Collection(); + } +} 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 @@ +items); @@ -32,6 +33,7 @@ class Bar implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -51,6 +53,7 @@ class Baz implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -70,6 +73,7 @@ class Lorem implements \IteratorAggregate /** @var array<\stdClass> */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -89,6 +93,7 @@ class Ipsum implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); diff --git a/tests/PHPStan/Rules/Methods/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php b/tests/PHPStan/Rules/Methods/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php new file mode 100644 index 0000000000..b938396cdf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php @@ -0,0 +1,32 @@ +|null + */ + public function doFoo(): ?array + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): array + { + + } + +} + +function (Bar $bar): void { + assertType('array', $bar->doFoo()); +}; 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/list-return-type-covariance.php b/tests/PHPStan/Rules/Methods/data/list-return-type-covariance.php new file mode 100644 index 0000000000..7ff8636ef3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/list-return-type-covariance.php @@ -0,0 +1,21 @@ + */ + public function returnsList(); + + /** @return array */ + public function returnsArray(); +} + +interface ListChild extends ListParent +{ + /** @return array */ + public function returnsList(); + + /** @return list */ + public function returnsArray(); +} diff --git a/tests/PHPStan/Rules/Methods/data/lowercase-string.php b/tests/PHPStan/Rules/Methods/data/lowercase-string.php new file mode 100644 index 0000000000..40c475e9e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/lowercase-string.php @@ -0,0 +1,33 @@ +acceptLowercaseString('NotLowerCase'); + $this->acceptLowercaseString('lowercase'); + $this->acceptLowercaseString($string); + $this->acceptLowercaseString($lowercaseString); + $this->acceptLowercaseString($numericString); + $this->acceptLowercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-serialization.php b/tests/PHPStan/Rules/Methods/data/magic-serialization.php new file mode 100644 index 0000000000..cadddfb876 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-serialization.php @@ -0,0 +1,30 @@ + */ + public function __serialize(): array + { + return []; + } + + /** @param array $data */ + public function __unserialize(array $data): void + { + } +} + +class WrongSignature { + + public function __serialize() + { + return ''; + } + + public function __unserialize($data) + { + return ''; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-signatures.php b/tests/PHPStan/Rules/Methods/data/magic-signatures.php new file mode 100644 index 0000000000..0d71caa470 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-signatures.php @@ -0,0 +1,70 @@ +pure1('test'); (new Bzz())->pure2('test'); (new Bzz())->pure3('test'); + (new Bzz())->pure4('test'); + (new Bzz())->pure5('test'); }; diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php new file mode 100644 index 0000000000..7549ebf390 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -0,0 +1,32 @@ +instanceMethod(); +$o?->instanceMethod(); + +(void)$o->instanceMethod(); +(void)$o?->instanceMethod(); + +foreach ($o->instanceMethod() as $num) { + var_dump($num); +} + +$o->differentCase(); + +$o->instanceMethod(...); 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/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 5b708cad89..480373825a 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php @@ -113,3 +113,35 @@ public function doFoo(): \Traversable } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function returnsGenericWithDefault(): GenericClassWithDefault + { + + } + + public function returnsGenericWithSomeDefaults(): GenericClassWithSomeDefaults + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php b/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php new file mode 100644 index 0000000000..a0c83d7e3f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php @@ -0,0 +1,35 @@ + + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self + */ + public function doFoo2(): void + { + + } + + /** + * @phpstan-self-out Foo&callable + */ + public function doFoo3(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php b/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php new file mode 100644 index 0000000000..688556c99b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php @@ -0,0 +1,17 @@ + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-serialization.php b/tests/PHPStan/Rules/Methods/data/missing-serialization.php new file mode 100644 index 0000000000..e4c53064d5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-serialization.php @@ -0,0 +1,40 @@ += 8.1 + +namespace MissingMagicSerializationMethods; + +use Serializable; + +abstract class abstractObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +class myObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +enum myEnum implements Serializable { + case X; + case Y; + + public function serialize() { + } + public function unserialize($data) { + } +} + +abstract class allGood implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } + public function __serialize() { + } + public function __unserialize($data) { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/named-arguments.php b/tests/PHPStan/Rules/Methods/data/named-arguments.php index f5c779f171..25d9ef362b 100644 --- a/tests/PHPStan/Rules/Methods/data/named-arguments.php +++ b/tests/PHPStan/Rules/Methods/data/named-arguments.php @@ -92,6 +92,7 @@ public function doDolor(): void $this->doIpsum(...['a' => 1, 'foo' => 'foo']); $this->doIpsum(...['b' => 1, 'foo' => 'foo']); $this->doIpsum(...[1, 2], 'foo'); + $this->doIpsum(1, 2, foo: 1, bar: 2); } } diff --git a/tests/PHPStan/Rules/Methods/data/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/nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php index dbe6ef395d..0eb89b8ae8 100644 --- a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php @@ -27,4 +27,11 @@ public function doLorem(?self $selfOrNull): void $this->doBaz($selfOrNull?->test->test); } + public function doNull(): void + { + $null = null; + $null->foo(); + $null?->foo(); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/object-shapes.php b/tests/PHPStan/Rules/Methods/data/object-shapes.php new file mode 100644 index 0000000000..8df09d5f93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/object-shapes.php @@ -0,0 +1,215 @@ +doBar(new stdClass()); + $this->doBar(new Exception()); + $this->doBar($e); + } + + /** + * @param object{foo: int, bar: string} $o + */ + public function doBar($o): void + { + + } + + /** + * @param object{foo: string, bar: int} $o + * @param object{foo?: int, bar: string} $p + * @param object{foo: int, bar: string} $q + */ + public function doBaz( + $o, + $p, + $q + ): void + { + $this->doBar($o); + $this->doBar($p); + $this->doBar($q); + + $this->requireStdClass($o); + $this->requireStdClass((object) []); + $this->doBar((object) ['foo' => 1, 'bar' => 'bar']); // OK + $this->doBar((object) ['foo' => 'foo', 'bar' => 1]); // Error + $this->acceptsObject($o); + } + + public function requireStdClass(stdClass $std): void + { + + } + + public function acceptsObject(object $o): void + { + $this->doBar($o); + $this->doBar(new \stdClass()); + } + +} + +class Bar +{ + + /** @var int */ + public $a; + + /** + * @param object{a: int} $o + */ + public function doFoo(object $o): void + { + $this->requireBar($o); + } + + public function requireBar(self $bar): void + { + $this->doFoo($bar); + $this->doBar($bar); + } + + /** + * @param object{a: string} $o + */ + public function doBar(object $o): void + { + + } + +} + +/** + * @property-write int $c + */ +#[\AllowDynamicProperties] +class Baz +{ + + /** @var int */ + protected $a; + + /** @var array{foo: int} */ + public $d; + + public function doFoo(): void + { + $this->doBar($this); + $this->doBaz($this); + $this->doLorem($this); + $this->doIpsum($this); + } + + /** + * @param object{a: int} $o + */ + public function doBar(object $o): void + { + + } + + /** @var int */ + public static $b; + + /** + * @param object{b: int} $o + */ + public function doBaz(object $o): void + { + + } + + /** + * @param object{c: int} $o + */ + public function doLorem(object $o): void + { + + } + + /** + * @param object{d: array{foo: string}} $o + */ + public function doIpsum(object $o): void + { + + } + +} + +class OptionalProperty +{ + + /** + * @param object{foo?: string} $o + */ + public function doFoo(object $o): void + { + $this->doBar($o); + $this->doBaz($o); + } + + /** + * @param object{foo?: int} $o + */ + public function doBar(object $o): void + { + + } + + /** + * @param object{foo: int} $o + */ + public function doBaz(object $o): void + { + + } + +} + +final class FinalClass +{ + +} + +class ClassWithFooIntProperty +{ + + /** @var int */ + public $foo; + +} + +class TestAcceptance +{ + + /** + * @param object{foo: int} $o + * @return void + */ + public function doFoo(object $o): void + { + + } + + public function doBar( + \Traversable $traversable, + FinalClass $finalClass, + ClassWithFooIntProperty $classWithFooIntProperty + ) + { + $this->doFoo($traversable); + $this->doFoo($finalClass); + $this->doFoo($classWithFooIntProperty); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/override-attribute.php b/tests/PHPStan/Rules/Methods/data/override-attribute.php new file mode 100644 index 0000000000..ca16bdba5b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/override-attribute.php @@ -0,0 +1,60 @@ + + */ +trait FooTrait +{ + /** + * Offset checker + * + * @phpstan-param Offset $offset + * @return bool + * @template Offset of key-of + */ + abstract public function offsetExists(mixed $offset): bool; +} + +/** + * @template DataArray of array + * @phpstan-type DataKey key-of + * @phpstan-type DataValue DataArray[DataKey] + */ +class FooClass +{ + + /** @phpstan-use FooTrait */ + use FooTrait; + + /** @phpstan-var DataArray|array{} */ + public array $data = []; + + + /** + * Data checker + * + * @phpstan-param Offset $offset + * @return bool + * @template Offset of key-of + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->data); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php b/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php new file mode 100644 index 0000000000..63bc6f403f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php @@ -0,0 +1,42 @@ += 8.0 + +namespace OverridingIndirectPrototype; + +class Foo +{ + + public function doFoo(): mixed + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): string + { + + } + +} + +class Baz extends Bar +{ + + public function doFoo(): mixed + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php new file mode 100644 index 0000000000..8a84a36c65 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php @@ -0,0 +1,38 @@ += 8.0 + +namespace OverridingTraitMethodsPhpDoc; + +trait Foo +{ + + public function doFoo(int $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + /** + * @param positive-int $i + */ + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + /** + * @param non-empty-string $i + */ + public function doBar(string $i): int + { + // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php new file mode 100644 index 0000000000..6e66526370 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php @@ -0,0 +1,84 @@ += 8.0 + +namespace OverridingTraitMethods; + +trait Foo +{ + + public function doFoo(string $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + public function doBar(int $i): int + { + // error + } + +} + +trait FooPrivate +{ + + abstract private function doBar(string $i): int; + +} + +class Baz +{ + use FooPrivate; + + private function doBar(int $i): int + { + + } +} + +class Lorem +{ + use Foo; + + protected function doBar(string $i): int + { + + } +} + +class Ipsum +{ + use Foo; + + public static function doBar(string $i): int + { + + } +} + +trait FooStatic +{ + abstract public static function doBar(string $i): int; +} + +class Dolor +{ + use FooStatic; + + public function doBar(string $i): int + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php b/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php new file mode 100644 index 0000000000..f36ffbfb1f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php @@ -0,0 +1,32 @@ += 8.4 + +namespace PropertyHooksReturn; + +class Foo +{ + + public int $i { + get { + if (rand(0, 1)) { + return 'foo'; + } + + return 1; + } + set { + if (rand(0, 1)) { + return; + } + + return 1; + } + } + + /** @var non-empty-string */ + public string $s { + get { + if (rand(0, 1)) { + return ''; + } + + return 'foo'; + } + } + +} + +/** + * @template T of Foo + */ +class GenericFoo +{ + + /** @var T */ + public Foo $a { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->a; + } + } + + /** + * @param T $c + */ + public function __construct( + /** @var T */ + public Foo $b { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->b; + } + }, + + public Foo $c { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->c; + } + } + ) + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php new file mode 100644 index 0000000000..3c801d2090 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php @@ -0,0 +1,68 @@ +acceptsCallable($cb); + $this->acceptsCallable($pureCb); + $this->acceptsPureCallable($cb); + $this->acceptsPureCallable($pureCb); + $this->acceptsInt($cb); + $this->acceptsInt($pureCb); + + $this->acceptsPureCallable(function (): int { + return 1; + }); + $this->acceptsPureCallable(function (): int { + sleep(1); + + return 1; + }); + } + + /** + * @param pure-Closure $cb + */ + public function acceptsPureClosure(\Closure $cb): void + { + + } + + public function doFoo2(): void + { + $this->acceptsPureClosure(function (): int { + return 1; + }); + $this->acceptsPureClosure(function (): int { + sleep(1); + + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 index 647a05f3af..d93cdd1bd8 100644 --- a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ - 8.0 namespace RequiredAfterOptional; @@ -22,4 +22,31 @@ public function doLorem(bool $foo = true, $bar): void // not OK { } + public function doDolor(?int $foo = 1, $bar): void // not OK + { + } + + public function doSit(?int $foo = null, $bar): void // not OK + { + } + + public function doAmet(int|null $foo = 1, $bar): void // not OK + { + } + + public function doConsectetur(int|null $foo = null, $bar): void // not OK + { + } + + public function doAdipiscing(mixed $foo = 1, $bar): void // not OK + { + } + + public function doElit(mixed $foo = null, $bar): void // not OK + { + } + + public function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK + { + } } diff --git a/tests/PHPStan/Rules/Methods/data/return-list-rule.php b/tests/PHPStan/Rules/Methods/data/return-list-rule.php new file mode 100644 index 0000000000..4827058bdf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list-rule.php @@ -0,0 +1,87 @@ + + */ +class BinaryOpEnumValueRule implements Rule +{ + + /** @var class-string */ + private string $className; + + /** + * @param class-string $operator + */ + public function __construct(string $operator, ?string $okMessage = null) + { + $this->className = $operator; + } + + public function getNodeType(): string + { + return $this->className; + } + + /** + * @param BinaryOp $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + $isDirectCompareType = true; + + if (!$this->isEnumWithValue($leftType) || !$this->isEnumWithValue($rightType)) { + $isDirectCompareType = false; + } + + $errors = []; + $leftError = $this->processOpExpression($node->left, $leftType, $node->getOperatorSigil()); + $rightError = $this->processOpExpression($node->right, $rightType, $node->getOperatorSigil()); + + if ($leftError !== null) { + $errors[] = $leftError; + } + + if ($rightError !== null && $rightError !== $leftError) { + $errors[] = $rightError; + } + + if (!$isDirectCompareType && $errors === []) { + return []; + } + + if ($isDirectCompareType && $errors === []) { + $errors[] = sprintf( + 'Cannot compare %s to %s', + $leftType->describe(VerbosityLevel::typeOnly()), + $rightType->describe(VerbosityLevel::typeOnly()), + ); + } + + return array_map(static fn (string $message) => RuleErrorBuilder::message($message)->build(), $errors); + } + + private function processOpExpression(Expr $expression, Type $expressionType, string $sigil): ?string + { + return null; + } + + private function isEnumWithValue(Type $type): bool + { + return false; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-list.php b/tests/PHPStan/Rules/Methods/data/return-list.php new file mode 100644 index 0000000000..1e13f0b7bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list.php @@ -0,0 +1,21 @@ + */ + public function getList1(): array + { + return array_filter(['foo', 'bar'], 'file_exists'); + } + + /** + * @param array $array + * @return list + */ + public function getList2(array $array): array + { + return array_intersect_key(['foo', 'bar'], $array); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php b/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php new file mode 100644 index 0000000000..129ad19e24 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php @@ -0,0 +1,49 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + return [ + RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - returns plain string + return ['foo']; + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - missing identifier + return [ + RuleErrorBuilder::message('foo') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - not a list + return [ + 1 => RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php new file mode 100644 index 0000000000..59a5d51884 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php @@ -0,0 +1,31 @@ += 8.3 + +namespace ReturnTypeClassConstant; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + + const static FOO = Foo::A; + + case A; + + public function returnStatic(): static + { + assertType('ReturnTypeClassConstant\Foo::A', self::FOO); + return self::FOO; + } + + public function returnStatic2(self $self): static + { + assertType('ReturnTypeClassConstant\Foo::A', $self::FOO); + return $self::FOO; + } + +} + +function (Foo $foo): void { + assertType('ReturnTypeClassConstant\Foo::A', Foo::FOO); + assertType('ReturnTypeClassConstant\Foo::A', $foo::FOO); +}; diff --git a/tests/PHPStan/Rules/Methods/data/returnTypes.php b/tests/PHPStan/Rules/Methods/data/returnTypes.php index bc5bfc2742..e7030f8ad8 100644 --- a/tests/PHPStan/Rules/Methods/data/returnTypes.php +++ b/tests/PHPStan/Rules/Methods/data/returnTypes.php @@ -375,7 +375,7 @@ public function misleadingIntReturnType(): \ReturnTypes\integer } } - public function misleadingMixedReturnType(): mixed + /*public function misleadingMixedReturnType(): mixed { if (rand(0, 1)) { return 1; @@ -386,7 +386,7 @@ public function misleadingMixedReturnType(): mixed if (rand(0, 1)) { return new mixed(); } - } + }*/ } class FooChild extends Foo @@ -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([]); @@ -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); @@ -1253,3 +1255,28 @@ public function doBaz3(): string } } + +interface MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int|numeric-string + */ + function exec(\mysqli $connection, string $sql); +} + +final class MySQLiAffectedRowsReturnType implements MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int<0, max>|numeric-string + */ + function exec(\mysqli $mysqli, string $sql) + { + $result = $mysqli->query($sql); + + if ($result === false || 0 > $mysqli->affected_rows) { + throw new \RuntimeException(); + } + + return $mysqli->affected_rows; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/rule-error-signature.php b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php new file mode 100644 index 0000000000..1fae783da6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php @@ -0,0 +1,132 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + // also ok + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return (string|RuleError)[] errors + */ + public function processNode(Node $node, Scope $scope): array + { + // old return type - not ok + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return string[] + */ + public function processNode(Node $node, Scope $scope): array + { + // just strings - not ok + } + +} + +/** + * @implements Rule + */ +class Ipsum implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // no identifiers - not ok + } + +} + +/** + * @implements Rule + */ +class Dolor implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // not a list - not ok + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/self-out.php b/tests/PHPStan/Rules/Methods/data/self-out.php new file mode 100644 index 0000000000..887ee2fbb0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/self-out.php @@ -0,0 +1,37 @@ += 8.4 + +namespace ShortGetPropertyHookReturn; + +class Foo +{ + + public int $i { + get => 'foo'; + } + + public int $i2 { + get => 1; + } + + /** @var non-empty-string */ + public string $s { + get => ''; + } + + /** @var non-empty-string */ + public string $s2 { + get => 'foo'; + } + +} + +/** + * @template T of Foo + */ +class GenericFoo +{ + + /** @var T */ + public Foo $a { + get => new Foo(); + } + + /** @var T */ + public Foo $a2 { + get => $this->a2; + } + + /** + * @param T $c + */ + public function __construct( + /** @var T */ + public Foo $b { + get => new Foo(); + }, + + /** @var T */ + public Foo $b2 { + get => $this->b2; + }, + + public Foo $c { + get => new Foo(); + }, + + public Foo $c2 { + get => $this->c2; + } + ) + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php new file mode 100644 index 0000000000..1251bf5933 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php @@ -0,0 +1,20 @@ +escapeInput($value), $namespace); + } + + private function escapeInput(?string $value): ?string + { + if ($value === null) { + return null; + } + return htmlspecialchars((string) normalizer_normalize($value), ENT_XML1, 'UTF-8'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/static-has-method.php b/tests/PHPStan/Rules/Methods/data/static-has-method.php new file mode 100644 index 0000000000..177bde0ab4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-has-method.php @@ -0,0 +1,51 @@ += 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/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-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-nodiscard.php b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php new file mode 100644 index 0000000000..5211ed9f3f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php @@ -0,0 +1,48 @@ + $v + */ + public function foo($p, $v): void + { + } +} + +function (HelloWorld $foo) { + $foo->foo(0, 0); + $foo->foo('', 0); + $foo->foo([], 0); + $foo->foo([1], 0); + $foo->foo([1], 1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/uppercase-string.php b/tests/PHPStan/Rules/Methods/data/uppercase-string.php new file mode 100644 index 0000000000..de7500ee8c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/uppercase-string.php @@ -0,0 +1,33 @@ +acceptUppercaseString('NotUpperCase'); + $this->acceptUppercaseString('UPPERCASE'); + $this->acceptUppercaseString($string); + $this->acceptUppercaseString($uppercaseString); + $this->acceptUppercaseString($numericString); + $this->acceptUppercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php new file mode 100644 index 0000000000..e5cd99d7a4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php @@ -0,0 +1,4 @@ += 8.0 + +$foo->regularCall(); +$foo?->nullsafeCall(); diff --git a/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php new file mode 100644 index 0000000000..73ea93d662 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php @@ -0,0 +1,30 @@ + + */ + public function doFoo(): array + { + return $this->listOfBars(); + } + + /** + * @return list + */ + public function listOfBars(): array + { + return []; + } + +} + +class Test2 +{ + + /** + * @return non-empty-array + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return non-empty-array + */ + public function nonEmptyArrayOfBars(): array + { + /** @var Bar $b */ + $b = doFoo(); + return [$b]; + } + +} + +class Test3 +{ + + /** + * @return non-empty-list + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return array + */ + public function nonEmptyArrayOfBars(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 2b2640ee7b..b848a761d7 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -4,18 +4,18 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MissingReturnRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixedMissingReturn; + private bool $checkExplicitMixedMissingReturn; - /** @var bool */ - private $checkPhpDocMissingReturn = true; + private bool $checkPhpDocMissingReturn = true; protected function getRule(): Rule { @@ -41,7 +41,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.', @@ -107,6 +111,18 @@ public function testRule(): void 'Method MissingReturn\NeverReturn::doBaz2() should always throw an exception or terminate script execution but doesn\'t do that.', 481, ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 514, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 515, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo2() should return int but return statement is missing.', + 524, + ], ]); } @@ -138,6 +154,13 @@ public function testMissingMixedReturnInEmptyBody(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug3488(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-3488.php'], []); + } + public function testBug3669(): void { $this->checkExplicitMixedMissingReturn = true; @@ -146,7 +169,7 @@ public function testBug3669(): void $this->analyse([__DIR__ . '/data/bug-3669.php'], []); } - public function dataCheckPhpDocMissingReturn(): array + public static function dataCheckPhpDocMissingReturn(): array { return [ [ @@ -235,22 +258,18 @@ public function dataCheckPhpDocMissingReturn(): array } /** - * @dataProvider dataCheckPhpDocMissingReturn - * @param bool $checkPhpDocMissingReturn - * @param mixed[] $errors + * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataCheckPhpDocMissingReturn')] public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkExplicitMixedMissingReturn = true; $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; $this->analyse([__DIR__ . '/data/check-phpdoc-missing-return.php'], $errors); } - public function dataModelMixin(): array + public static function dataModelMixin(): array { return [ [ @@ -262,19 +281,13 @@ public function dataModelMixin(): array ]; } - /** - * @dataProvider dataModelMixin - * @param bool $checkExplicitMixedMissingReturn - */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataModelMixin')] public function testModelMixin(bool $checkExplicitMixedMissingReturn): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; $this->checkPhpDocMissingReturn = true; - $this->analyse([__DIR__ . '/../../Analyser/data/model-mixin.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/model-mixin.php'], [ [ 'Method ModelMixin\Model::__callStatic() should return mixed but return statement is missing.', 13, @@ -282,4 +295,82 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug6257(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12722(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-12722.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Missing/data/bug-12722.php b/tests/PHPStan/Rules/Missing/data/bug-12722.php new file mode 100644 index 0000000000..6c42edbd19 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-12722.php @@ -0,0 +1,32 @@ += 8.1 + +namespace Bug12722; + +enum states { + case state1; + case statealmost1; + case state3; +} + +class HelloWorld +{ + public function intentional_fallthrough(states $state): int + { + switch($state) { + + case states::state1: //intentional fall-trough this case... + case states::statealmost1: return 1; + case states::state3: return 3; + } + } + + public function no_fallthrough(states $state): int + { + switch($state) { + + case states::state1: return 1; + case states::statealmost1: return 1; + case states::state3: return 3; + } + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-3488-2.php b/tests/PHPStan/Rules/Missing/data/bug-3488-2.php new file mode 100644 index 0000000000..87d3466236 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-3488-2.php @@ -0,0 +1,52 @@ += 8.1 + +namespace Bug3488; + +enum EnumWithThreeCases { + case ValueA; + case ValueB; + case ValueC; +} + +function testFunction(EnumWithThreeCases $var) : int +{ + switch ($var) { + case EnumWithThreeCases::ValueA: + // some other code + return 1; + case EnumWithThreeCases::ValueB: + // some other code + return 2; + case EnumWithThreeCases::ValueC: + // some other code + return 3; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-6257.php b/tests/PHPStan/Rules/Missing/data/bug-6257.php new file mode 100644 index 0000000000..acb1023c88 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-6257.php @@ -0,0 +1,31 @@ += 8.0 + +namespace ReturnTypes; + +/** + * @return never + */ +function alwaysThrow() { + match(true) { + true => throw new \Exception(), + }; +} + +/** + * @return never + */ +function alwaysThrow2() { + match(rand(0, 1)) { + 0 => throw new \Exception(), + }; +} + +/** + * @return never + */ +function sometimesThrows() { + match(rand(0, 1)) { + 0 => throw new \Exception(), + default => 'test', + }; +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-6807.php b/tests/PHPStan/Rules/Missing/data/bug-6807.php new file mode 100644 index 0000000000..d7ffdde9dd --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-6807.php @@ -0,0 +1,16 @@ + 5) + throw new Exception(); + + if (rand() == 1) + return 5; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-7384.php b/tests/PHPStan/Rules/Missing/data/bug-7384.php new file mode 100644 index 0000000000..3ef533d5bc --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-7384.php @@ -0,0 +1,26 @@ += 8.4 + +namespace PropertyHooksMissingReturn; + +class Foo +{ + + public int $i { + get { + if (rand(0, 1)) { + + } else { + return 1; + } + } + + set { + // set hook returns void + } + } + + public int $j { + get { + + } + } + + public int $ok { + get { + return $this->ok + 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php new file mode 100644 index 0000000000..ce58341c06 --- /dev/null +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -0,0 +1,92 @@ + + */ +final class UsedNamesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsedNamesRule(); + } + + public function testSimpleUses(): void + { + $this->analyse([__DIR__ . '/data/simple-uses.php'], [ + [ + 'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.', + 7, + ], + ]); + } + + public function testGroupedUses(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses.php'], [ + [ + 'Cannot declare interface SomeNamespace\GroupedUses because the name is already in use.', + 10, + ], + ]); + } + + public function testSimpleUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/simple-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as SimpleUsesUnderClass because the name is already in use.', + 9, + ], + ]); + } + + public function testGroupedUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\FooBar as FooBar because the name is already in use.', + 14, + ], + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as GroupedUsesUnderClass because the name is already in use.', + 15, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 5, + ], + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 9, + ], + ]); + } + + public function testMultipleNamespaces(): void + { + $this->analyse([__DIR__ . '/data/multiple-namespaces.php'], [ + [ + 'Cannot declare trait FirstNamespace\MultipleNamespaces because the name is already in use.', + 24, + ], + ]); + } + + public function testIgnoreUseFunctionAndConstant(): void + { + $this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php new file mode 100644 index 0000000000..0c96a7b5ac --- /dev/null +++ b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php @@ -0,0 +1,16 @@ + + * @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 = self::createReflectionProvider(); + return new ExistingNamesInGroupUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index dc0750e851..e78cd77e65 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), true); + $reflectionProvider = self::createReflectionProvider(); + return new ExistingNamesInUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); } public function testRule(): void @@ -45,4 +59,17 @@ public function testRule(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 14, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/NodeConnectingRule.php b/tests/PHPStan/Rules/NodeConnectingRule.php deleted file mode 100644 index 9629e48986..0000000000 --- a/tests/PHPStan/Rules/NodeConnectingRule.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -class NodeConnectingRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Stmt\Echo_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - return [ - sprintf( - 'Parent: %s, previous: %s, next: %s', - get_class($node->getAttribute('parent')), - get_class($node->getAttribute('previous')), - get_class($node->getAttribute('next')) - ), - ]; - } - -} diff --git a/tests/PHPStan/Rules/NodeConnectingRuleTest.php b/tests/PHPStan/Rules/NodeConnectingRuleTest.php deleted file mode 100644 index 410b58b36a..0000000000 --- a/tests/PHPStan/Rules/NodeConnectingRuleTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -class NodeConnectingRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new NodeConnectingRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/node-connecting.php'], [ - [ - 'Parent: PhpParser\Node\Stmt\If_, previous: PhpParser\Node\Stmt\Switch_, next: PhpParser\Node\Stmt\Foreach_', - 11, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php index 3bda92c21b..8dfd60fc26 100644 --- a/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/invalid-assign-var.php'], [ [ 'Nullsafe operator cannot be on left side of assignment.', diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index e72e1f3364..bfca28ee4f 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), ); } @@ -94,21 +103,169 @@ public function testRule(): void 122, ], [ - 'Binary operation "." between array and \'xyz\' results in an error.', + 'Binary operation "." between array and \'xyz\' results in an error.', 127, ], [ - 'Binary operation "." between array|string and \'xyz\' results in an error.', + 'Binary operation "." between array{}|non-falsy-string and \'xyz\' results in an error.', 134, ], [ - 'Binary operation "+" between (array|string) and 1 results in an error.', + 'Binary operation "+" between (array|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, + ], ]); } @@ -122,4 +279,539 @@ public function testBug3515(): void $this->analyse([__DIR__ . '/data/bug-3515.php'], []); } + public function testBug8827(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBinaryMixed(): void + { + $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, + ], + ]); + } + + public function testBug10595(): void + { + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-10595.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php index ef493ca348..3aec604741 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -2,18 +2,26 @@ namespace PHPStan\Rules\Operators; +use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidComparisonOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidComparisonOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkUnion = true; + + protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnion, false, false, false, true), + $this->getContainer()->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + true, ); } @@ -140,7 +148,71 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $this->analyse([__DIR__ . '/data/invalid-comparison-nullsafe.php'], [ + [ + 'Comparison operation "==" between stdClass|null and int results in an error.', + 12, + ], + ]); + } + + public function testBug3364(): void + { + $this->checkUnion = false; + $this->analyse([__DIR__ . '/data/bug-3364.php'], [ + [ + 'Comparison operation "!=" between array|null and 1 results in an error.', + 18, + ], + [ + 'Comparison operation "!=" between object|null and 1 results in an error.', + 26, + ], ]); } + public function testBug11119(): void + { + $this->analyse([__DIR__ . '/data/bug-11119.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testBug13001(): void + { + $this->analyse([__DIR__ . '/data/bug-13001.php'], [ + [ + 'Comparison operation ">" between BcMath\\Number and 0.2 results in an error.', + 10, + ], + [ + 'Comparison operation "<=>" between 0.2 and BcMath\\Number results in an error.', + 11, + ], + ]); + } + + public function testBug9386(): void + { + $this->analyse([__DIR__ . '/data/bug-9386.php'], []); + } + + public function testBug7280Comment(): void + { + $this->analyse([__DIR__ . '/data/bug-7280-comment.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index a5564251cd..4faae03e3c 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 PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), + ); } public function testRule(): void @@ -28,6 +39,109 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testMixed(): void + { + $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 77bb4fe9e3..7300106db7 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 PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @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(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), + ); } public function testRule(): void @@ -33,9 +44,128 @@ public function testRule(): void 20, ], [ - 'Unary operation "~" on array() results in an error.', + '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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testMixed(): void + { + $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-13001.php b/tests/PHPStan/Rules/Operators/data/bug-13001.php new file mode 100644 index 0000000000..bd8ac1b2f5 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-13001.php @@ -0,0 +1,87 @@ += 8.4 + +namespace Bug13001; + +$bcNumber = new \BcMath\Number(-1); + +// invalid + +var_dump( + $bcNumber > 0.2, + 0.2 <=> $bcNumber, +); + +// probably invalid, but PHPStan currently allows these comparisons: + +var_dump( + $bcNumber < true, + null <= $bcNumber, + fopen('php://stdin', 'r') >= $bcNumber, + new \stdClass() == $bcNumber, +); + +// valid + +var_dump( + $bcNumber < 0, + $bcNumber < '0.2', + $bcNumber < new \BcMath\Number(3), +); +var_dump( + $bcNumber <= 0, + $bcNumber <= '0.2', + $bcNumber <= new \BcMath\Number(3), +); +var_dump( + $bcNumber > 0, + $bcNumber > '0.2', + $bcNumber > new \BcMath\Number(3), +); +var_dump( + $bcNumber >= 0, + $bcNumber >= '0.2', + $bcNumber >= new \BcMath\Number(3), +); +var_dump( + $bcNumber == 0, + $bcNumber == '0.2', + $bcNumber == new \BcMath\Number(3), +); +var_dump( + $bcNumber != 0, + $bcNumber != '0.2', + $bcNumber != new \BcMath\Number(3), +); +var_dump( + $bcNumber <=> 0, + $bcNumber <=> '0.2', + $bcNumber <=> new \BcMath\Number(3), +); +var_dump( + 0 < $bcNumber, + '0.2' < $bcNumber, +); +var_dump( + 0 <= $bcNumber, + '0.2' <= $bcNumber, +); +var_dump( + 0 > $bcNumber, + '0.2' > $bcNumber, +); +var_dump( + 0 >= $bcNumber, + '0.2' >= $bcNumber, +); +var_dump( + 0 == $bcNumber, + '0.2' == $bcNumber, +); +var_dump( + 0 != $bcNumber, + '0.2' != $bcNumber, +); +var_dump( + 0 <=> $bcNumber, + '0.2' <=> $bcNumber, +); diff --git a/tests/PHPStan/Rules/Operators/data/bug-3364.php b/tests/PHPStan/Rules/Operators/data/bug-3364.php new file mode 100644 index 0000000000..764b8c0b96 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-3364.php @@ -0,0 +1,42 @@ +|null $value + */ + public function transform($value) { + $value != 1; + } + + /** + * @param array|null $value + */ + public function transform2($value) { + $value != 1; + } + + + /** + * @param object|null $value + */ + public function transform3($value) { + $value != 1; + } + + /** + * @param array|object $value + */ + public function transform4($value) { + $value != 1; + } + + /** + * @param null $value + */ + public function transform5($value) { + $value != 1; + } +} diff --git a/tests/PHPStan/Rules/Operators/data/bug-5309.php b/tests/PHPStan/Rules/Operators/data/bug-5309.php new file mode 100644 index 0000000000..3ed4a5a833 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-5309.php @@ -0,0 +1,17 @@ + 0) { + $x += 1; + } + if ($x > 0) { + return 5 / $x; + } + + return 1.0; +} + diff --git a/tests/PHPStan/Rules/Operators/data/bug-7280-comment.php b/tests/PHPStan/Rules/Operators/data/bug-7280-comment.php new file mode 100644 index 0000000000..7d194d4a03 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-7280-comment.php @@ -0,0 +1,20 @@ + $numbers + */ + $numbers = [1, 2]; + + $sum = array_reduce( + $numbers, + fn ($curr, $n) => $curr + $n, + 0 + ); + + if ($sum > 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 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug9386; + +trait BaseTrait { + protected false|int $_pos; + + public function myMethod():bool { + $pos = $this->_pos; + if ($pos === false) + return false; + if (($this instanceof BaseClass) && $this->length !== null) + return $pos >= $this->offset + $this->length; + return false; + } +} + +class BaseClass +{ + use BaseTrait; + protected ?int $length = null; + protected int $offset = 0; +} +class SecondClass +{ + use BaseTrait; +} 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..93ad74a014 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php @@ -0,0 +1,82 @@ + + */ +class FunctionAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new FunctionAssertRule(new AssertRuleHelper( + $reflectionProvider, + new UnresolvableTypeHelper(), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new MissingTypehintCheck(true, []), + new GenericObjectTypeCheck(), + true, + true, + )); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/function-assert.php'; + $this->analyse([__DIR__ . '/data/function-assert.php'], [ + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 8, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 17, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 26, + ], + [ + 'Asserted type int|string for $i with type int does not narrow down the type.', + 42, + ], + [ + 'Asserted type string for $i with type int can never happen.', + 49, + ], + [ + 'Assert references unknown parameter $j.', + 56, + ], + [ + 'Asserted negated type int for $i with type int can never happen.', + 63, + ], + [ + 'Asserted negated type string for $i with type int does not narrow down the type.', + 70, + ], + [ + 'PHPDoc tag @phpstan-assert for $array has no value type specified in iterable type list.', + 88, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php new file mode 100644 index 0000000000..0d4b491158 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php @@ -0,0 +1,71 @@ + + */ +class FunctionConditionalReturnTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FunctionConditionalReturnTypeRule(new ConditionalReturnTypeRuleHelper()); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/function-conditional-return-type.php'; + $this->analyse([__DIR__ . '/data/function-conditional-return-type.php'], [ + [ + 'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.', + 37, + ], + [ + 'Conditional return type references unknown parameter $j.', + 45, + ], + [ + 'Condition "int is int" in conditional return type is always true.', + 53, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 63, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 73, + ], + [ + 'Condition "int is not int" in conditional return type is always false.', + 81, + ], + [ + 'Condition "int is string" in conditional return type is always false.', + 89, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 99, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 109, + ], + [ + 'Condition "int is not string" in conditional return type is always true.', + 117, + ], + ]); + } + + public function testBug8609(): void + { + $this->analyse([__DIR__ . '/data/bug-8609-function.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php index 71e78e7ef6..1bd66e2600 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -25,22 +26,31 @@ public function testRule(): void 9, ], [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::BAZ with type string is incompatible with value 1.', - 17, - ], - [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR with type IncompatibleClassConstantPhpDoc\Foo is incompatible with value 1.', - 26, + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR contains generic type IncompatibleClassConstantPhpDoc\Foo but class IncompatibleClassConstantPhpDoc\Foo is not generic.', + 12, ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testNativeType(): void + { + $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc-native-type.php'], [ [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR contains generic type IncompatibleClassConstantPhpDoc\Foo but class IncompatibleClassConstantPhpDoc\Foo is not generic.', - 26, + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::BAZ with type string is incompatible with native type int.', + 14, ], [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Bar::BAZ with type string is incompatible with value 2.', - 35, + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::LOREM with type int|string is not subtype of native type int.', + 17, ], ]); } + #[RequiresPhp('>= 8.3')] + public function testBug10911(): void + { + $this->analyse([__DIR__ . '/data/bug-10911.php'], []); + } + } 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 163f826ac9..acac8c7dea 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -2,21 +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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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 = self::createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new GenericObjectTypeCheck(), - new UnresolvableTypeHelper() + new IncompatiblePhpDocTypeCheck( + new GenericObjectTypeCheck(), + new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + ), ); } @@ -70,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.', @@ -143,6 +172,37 @@ public function testRule(): void 'PHPDoc tag @return contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', 274, ], + [ + 'PHPDoc tag @param for parameter $i with type TFoo is not subtype of native type int.', + 283, + 'Write @template TFoo of int to fix this.', + ], + [ + '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, + ], ]); } @@ -158,11 +218,252 @@ public function testBug3753(): void '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.', + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testValueOfEnum(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testConditionalReturnType(): void + { + $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 $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 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 @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 @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, + ], [ - 'PHPDoc tag @param for parameter $bars contains unresolvable type.', - 28, + '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, + ], + ]); + } + + public function testBug13452(): void + { + $this->analyse([__DIR__ . '/data/bug-13452.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..8dd6faa5d2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php @@ -0,0 +1,88 @@ + + */ +class IncompatiblePropertyHookPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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, + ), + ), + ), + ); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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 164c03193e..0e6c44f8ff 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(), new UnresolvableTypeHelper()); + $reflectionProvider = self::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,15 +107,16 @@ 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 { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/incompatible-property-promoted.php'], [ [ 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$bar contains unresolvable type.', @@ -122,16 +154,43 @@ public function testPromotedProperties(): void 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$unknownClassConstant2 contains unresolvable type.', 49, ], + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\BazWithProperty::$bar with type string is incompatible with native type int.', + 61, + ], ]); } public function testBug4227(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-4227.php'], []); } + public function testBug7240(): void + { + $this->analyse([__DIR__ . '/data/bug-7240.php'], []); + } + + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callable-properties.php'], [ + [ + 'PHPDoc tag @var template T of Closure(T): T shadows @template T for class GenericCallableProperties\Test.', + 16, + ], + [ + 'PHPDoc tag @var template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 21, + ], + [ + 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 26, + ], + [ + 'PHPDoc tag @var template 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..6b5ba10a37 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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,39 @@ 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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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 ba8b2a014f..81c0868e3e 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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,72 +26,76 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], [ [ - 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13', - 25, + 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13 on line 2', + 6, ], [ - 'PHPDoc tag @param has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 24', - 25, + 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72 on line 5', + 9, ], [ - 'PHPDoc tag @param has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 43', - 25, + 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105 on line 6', + 10, ], [ - 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72', - 25, + 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127 on line 7', + 11, ], [ - 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105', - 25, + 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156 on line 9', + 13, ], [ - 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127', - 25, + 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165 on line 10', + 14, ], [ - 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156', - 25, + 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182 on line 11', + 15, ], [ - 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165', - 25, + 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208 on line 13', + 17, ], [ - 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182', - 25, + 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220 on line 14', + 18, ], [ - 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208', - 25, + 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251 on line 15', + 19, ], [ - 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220', - 25, + 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9 on line 1', + 28, ], [ - 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251', - 25, + 'PHPDoc tag @var has invalid value (callable(int)): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 17 on line 1', + 58, ], [ - 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9', - 29, + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 61, ], - /*[ - 'PHPDoc tag @var has invalid value ...', - 59, - ],*/ [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 62, + 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24 on line 1', + 71, ], [ - 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24', - 72, + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 80, ], [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 81, + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 88, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 91, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 101, ], ]); } @@ -103,4 +110,50 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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 cdeb22e447..1c3f33d575 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -3,30 +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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = self::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, true), + new MissingTypehintCheck(true, []), new UnresolvableTypeHelper(), true, - true + true, + true, ); } @@ -85,16 +94,28 @@ public function testRule(): void [ 'PHPDoc tag @var for variable $test has no value type specified in iterable type array.', 58, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGeneric but does not specify its types: T, U', 61, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', 67, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 73, + ], + [ + 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGenericWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 79, + ], + [ + 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 85, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); @@ -104,12 +125,12 @@ public function testBug4486(): void { $this->analyse([__DIR__ . '/data/bug-4486.php'], [ [ - 'PHPDoc tag @var for variable $one contains unknown class Bug4486\ClassName1.', + 'PHPDoc tag @var for variable $one contains unknown class Some\Namespaced\ClassName1.', 10, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'PHPDoc tag @var for variable $two contains unknown class Bug4486\ClassName2.', + 'PHPDoc tag @var for variable $two contains unknown class Some\Namespaced\ClassName2.', 10, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -125,7 +146,7 @@ public function testBug4486Namespace(): void { $this->analyse([__DIR__ . '/data/bug-4486-ns.php'], [ [ - 'PHPDoc tag @var for variable $one contains unknown class ClassName1.', + 'PHPDoc tag @var for variable $one contains unknown class Bug4486Namespace\ClassName1.', 6, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -137,4 +158,26 @@ public function testBug4486Namespace(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug6252(): void + { + $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 081fa77651..60cf6874c7 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -2,16 +2,23 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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)); } @@ -72,40 +79,57 @@ public function testInheritedPhpDocs(): void ]); } - public function dataMergeInheritedPhpDocs(): array + 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 static 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', ], ]; } - /** - * @dataProvider dataMergeInheritedPhpDocs - * @param string $className - * @param string $method - * @param string $expectedType - */ + #[DataProvider('dataMergeInheritedPhpDocs')] public function testMergeInheritedPhpDocs( string $className, string $method, - string $expectedType + string $expectedType, ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $method = $reflection->getNativeMethod($method); $throwsType = $method->getThrowType(); @@ -113,4 +137,15 @@ public function testMergeInheritedPhpDocs( $this->assertSame($expectedType, $throwsType->describe(VerbosityLevel::precise())); } + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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..72c156b2f8 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -0,0 +1,163 @@ + + */ +class MethodAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new MethodAssertRule(new AssertRuleHelper( + $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'], []); + } + + public function testBugIncompatibleAssertTypeWithMethodReturnType(): void + { + $this->analyse([__DIR__ . '/data/incompatible-assert-type-with-method-return-type.php'], [ + [ + 'Asserted type non-empty-list for $this->getValues() with type list can never happen.', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php new file mode 100644 index 0000000000..35ab5c2e4e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -0,0 +1,105 @@ + + */ +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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11939(): void + { + $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..8a9546216e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -0,0 +1,96 @@ + + */ +class RequireExtendsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new RequireExtendsDefinitionClassRule( + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..e345685e67 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -0,0 +1,66 @@ + + */ +class RequireExtendsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new RequireExtendsDefinitionTraitRule( + $reflectionProvider, + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ + [ + 'PHPDoc tag @phpstan-require-extends cannot contain final class IncompatibleRequireExtends\SomeFinalClass.', + 126, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type *NEVER*.', + 140, + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 171, + ], + [ + '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..807bbbc7d6 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php @@ -0,0 +1,35 @@ + + */ +class RequireImplementsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsDefinitionClassRule(); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..1d0421388f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php @@ -0,0 +1,69 @@ + + */ +class RequireImplementsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new RequireImplementsDefinitionTraitRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php new file mode 100644 index 0000000000..3ec5da8925 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -0,0 +1,55 @@ + + */ +class SealedDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new SealedDefinitionClassRule( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-sealed.php'], [ + [ + 'PHPDoc tag @phpstan-sealed is only valid on class or interface.', + 16, + ], + [ + 'PHPDoc tag @phpstan-sealed contains unknown class IncompatibleSealed\UnknownClass.', + 21, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-sealed contains unknown class IncompatibleSealed\UnknownClass.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..788a06f55a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionTraitRuleTest.php @@ -0,0 +1,33 @@ + + */ +class SealedDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + + return new SealedDefinitionTraitRule($reflectionProvider); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-sealed.php'], [ + [ + 'PHPDoc tag @phpstan-sealed is only valid on class or interface.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php new file mode 100644 index 0000000000..d515b2c8af --- /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), + self::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 12b5f8eaa8..513c9118a7 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -2,26 +2,48 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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), + self::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, @@ -62,65 +84,69 @@ public function testRule(): void 'Variable $foo in PHPDoc tag @var does not exist.', 109, ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 120, + ], [ 'Multiple PHPDoc @var tags above single variable assignment are not supported.', - 125, + 126, ], [ 'Variable $b in PHPDoc tag @var does not exist.', - 134, + 135, ], [ 'PHPDoc tag @var does not specify variable name.', - 155, + 156, ], [ 'PHPDoc tag @var does not specify variable name.', - 176, + 177, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 210, + 211, ], [ 'PHPDoc tag @var above foreach loop does not specify variable name.', - 234, + 235, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $bar in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 262, + 263, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 268, + 269, ], [ 'PHPDoc tag @var above assignment does not specify variable name.', - 274, + 275, ], [ 'Variable $slots in PHPDoc tag @var does not match assigned variable $itemSlots.', - 280, + 281, ], [ 'PHPDoc tag @var above a class has no effect.', - 300, + 301, ], [ 'PHPDoc tag @var above a method has no effect.', - 304, + 305, ], [ 'PHPDoc tag @var above a function has no effect.', - 312, + 313, ], ]); } @@ -173,4 +199,375 @@ 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testEnums(): void + { + $this->analyse([__DIR__ . '/data/wrong-var-enum.php'], [ + [ + 'PHPDoc tag @var above an enum has no effect.', + 13, + ], + ]); + } + + public static 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 static function dataPermutateCheckTypeAgainst(): iterable + { + yield [true]; + yield [false]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataReportWrongType')] + 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-10911.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10911.php new file mode 100644 index 0000000000..7cce5e8b88 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10911.php @@ -0,0 +1,27 @@ += 8.3 + +namespace Bug10911; + +abstract class Model +{ + /** + * The name of the "created at" column. + * + * @var string|null + */ + const CREATED_AT = 'created_at'; + + /** + * The name of the "updated at" column. + * + * @var string|null + */ + const UPDATED_AT = 'updated_at'; +} + +class TestModel extends Model +{ + const string CREATED_AT = 'data_criacao'; + const string UPDATED_AT = 'data_alteracao'; + const DELETED_AT = null; +} 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-13452.php b/tests/PHPStan/Rules/PhpDoc/data/bug-13452.php new file mode 100644 index 0000000000..7df34b8bbf --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-13452.php @@ -0,0 +1,23 @@ + $ref + */ + public function doFoo(ReflectionClass $ref): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php index 440577cae6..41bc50eae0 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php @@ -1,4 +1,4 @@ -= 7.4 + [], 'branches' => ['names' => [], 'exclude' => false]]; + } + else { + return 0; + } + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-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..165296e750 --- /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 $closure + */ +function testNull(Closure $closure): 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 nullReturn(): 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 $closure + */ + public function nullType(Closure $closure): 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 nullReturn(): 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 @@ + + */ + public function getValues(): array + { + + } + + /** + * @phpstan-assert-if-false non-empty-list $this->getValues() + */ + public function isEmpty(): bool + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc-native-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc-native-type.php new file mode 100644 index 0000000000..91fada5626 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc-native-type.php @@ -0,0 +1,19 @@ += 8.3 + +namespace IncompatibleClassConstantPhpDocNativeType; + +class Foo +{ + + public const int FOO = 1; + + /** @var positive-int */ + public const int BAR = 1; + + /** @var non-empty-string */ + public const int BAZ = 1; + + /** @var int|string */ + public const int LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php index c4f163e773..d2b9714425 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php @@ -8,45 +8,7 @@ class Foo /** @var self&\stdClass */ const FOO = 1; - /** @var int */ - const BAR = 1; - - const NO_TYPE = 'string'; - - /** @var string */ - const BAZ = 1; - - /** @var string|int */ - const LOREM = 1; - - /** @var int */ - const IPSUM = self::LOREM; // resolved to 1, I'd prefer string|int - /** @var self */ const DOLOR = 1; } - -class Bar extends Foo -{ - - const BAR = 2; - - const BAZ = 2; - -} - -class Baz -{ - - /** @var string */ - private const BAZ = 'foo'; - -} - -class Lorem extends Baz -{ - - private const BAZ = 1; - -} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php new file mode 100644 index 0000000000..01f3cda99a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php @@ -0,0 +1,30 @@ += 8.0 + +namespace IncompatibleConditionalReturnType; + +class Foo +{ + + /** + * @return ($p is int ? int : string) + */ + public function doFoo($p): int|string + { + + } + +} + + +class Bar +{ + + /** + * @return ($p is int ? int : string) + */ + public function doFoo($p): int + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php new file mode 100644 index 0000000000..79cd19f116 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php @@ -0,0 +1,80 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php new file mode 100644 index 0000000000..b1ce3b8762 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php @@ -0,0 +1,66 @@ += 8.4 + +namespace IncompatiblePropertyHookPhpDocTypes; + +class Foo +{ + + public int $i { + /** @return string */ + get { + return $this->i; + } + } + + public int $j { + /** @return string */ + set { + $this->j = 1; + } + } + + public int $k { + /** + * @param string $value + * @param-out int $value + */ + set { + $this->k = 1; + } + } + + public int $l { + /** @param \stdClass&\Exception $value */ + set { + + } + } + + public \Exception $m { + /** @param \Exception $value */ + set { + + } + } + +} + +/** @template T */ +class GenericFoo +{ + + public int $n { + /** @param int|callable(T): T $value */ + set (int|callable $value) { + + } + } + + public int $o { + /** @param int|callable<\stdClass>(T): T $value */ + set (int|callable $value) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php index 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 index f42989946a..ebf011438b 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php @@ -50,3 +50,17 @@ public function __construct( ) { } } + +class BazWithProperty +{ + + /** + * @param int $foo + * @param string $bar + */ + public function __construct(private int $foo, private int $bar) + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php new file mode 100644 index 0000000000..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-sealed.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-sealed.php new file mode 100644 index 0000000000..ab1f47ba28 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-sealed.php @@ -0,0 +1,41 @@ += 8.1 + +namespace IncompatibleSealed; + +class SomeClass {}; +interface SomeInterface {}; + +/** + * @phpstan-sealed SomeClass + */ +trait InvalidTrait1 {} + +/** + * @phpstan-sealed SomeClass + */ +enum InvalidEnum {} + +/** + * @phpstan-sealed UnknownClass + */ +class InvalidClass {} + +/** + * @phpstan-sealed UnknownClass + */ +interface InvalidInterface {} + +/** + * @phpstan-sealed SomeClass + */ +class Valid {} + +/** + * @phpstan-sealed SomeClass + */ +interface ValidInterface {} + +/** + * @phpstan-sealed SomeInterface + */ +interface ValidInterface2 {} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php new file mode 100644 index 0000000000..c60ff3ce6c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -0,0 +1,105 @@ + + */ + public function two($param); + + /** + * @phpstan-self-out int + */ + public function three(); + + /** + * @phpstan-self-out self|null + */ + public function four(); +} + +/** + * @template T + */ +class Foo +{ + + /** @phpstan-self-out self */ + public static function selfOutStatic(): void + { + + } + + /** + * @phpstan-self-out int&string + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self + */ + public function doBar(): void + { + + } + +} + +class GenericCheck +{ + + /** + * @phpstan-self-out self + */ + public function doFoo(): void + { + + } + +} + +/** + * @template T of \Exception + * @template U of int + */ +class GenericCheck2 +{ + + /** + * @phpstan-self-out self<\InvalidArgumentException> + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self<\InvalidArgumentException, positive-int, string> + */ + public function doFoo2(): void + { + + } + + /** + * @phpstan-self-out self<\InvalidArgumentException, string> + */ + public function doFoo3(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 517a3e0ac3..078e004d26 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -275,3 +275,57 @@ 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 15a61e603e..8b0cf8bd81 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php @@ -81,3 +81,25 @@ class ClassConstant const FOO = 1; } + +class AboveProperty +{ + + /** @var (Foo& */ + private $foo; + + /** @var (Foo& */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @var (Foo& */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php index fa2e510986..60a35c8178 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -36,3 +36,25 @@ function any() } } + +class AboveProperty +{ + + /** @phpstan-varr 1 */ + private $foo; + + /** @phpstan-varr 1 */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @phpstan-varr string */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php new file mode 100644 index 0000000000..1221fe7b43 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php @@ -0,0 +1,15 @@ += 8.4 + +namespace InvalidPHPStanTagPropertyHooks; + +class Foo +{ + + public int $i { + /** @phpstan-what what */ + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php new file mode 100644 index 0000000000..c40b13aa9f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php @@ -0,0 +1,22 @@ += 8.4 + +namespace InvalidThrowsPropertyHook; + +class Foo +{ + + public int $i { + /** @throws \InvalidArgumentException */ + get { + return 1; + } + } + + public int $j { + /** @throws \DateTimeImmutable */ + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php new file mode 100644 index 0000000000..b99b3e5577 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php @@ -0,0 +1,20 @@ + $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric<*> $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithDefault $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithSomeDefaults $test */ + $test = doFoo(); } public function doBar($foo) diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-assert.php b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php new file mode 100644 index 0000000000..ad07ee066d --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php @@ -0,0 +1,215 @@ +fooProp + */ + public function doFoo($a): bool + { + + } + + /** + * @phpstan-assert Nonexistent $a + * @phpstan-assert FooTrait $b + * @phpstan-assert fOO $c + * @phpstan-assert Foo $this->barProp + */ + public function doBar($a, $b, $c): bool + { + + } + + /** + * @phpstan-assert !null $this->fooProp + */ + public static function doBaz(): void + { + + } + +} + +trait FooTrait +{ + +} + +class InvalidGenerics +{ + + /** + * @phpstan-assert \Exception $m + */ + function invalidPhpstanAssertGeneric($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertWrongGenericParams($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertNotAllGenericParams($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertMoreGenericParams($m) { + + } + +} + + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class MissingTypes +{ + + /** + * @phpstan-assert array $m + */ + public function doFoo($m): void + { + + } + + /** + * @phpstan-assert FooBar $m + */ + public function doBar($m): void + { + + } + + /** + * @phpstan-assert callable $m + */ + public function doBaz($m): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php new file mode 100644 index 0000000000..7829e8c71a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php @@ -0,0 +1,189 @@ + ? true : false) + */ + public function foo(): bool + { + + } + +} + +class ParamOut +{ + + /** + * @param-out ($i is int ? 1 : 2) $out + */ + public function doFoo(int $i, &$out) { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php new file mode 100644 index 0000000000..3b705fbf07 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php @@ -0,0 +1,23 @@ +returnStatic(); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php b/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php new file mode 100644 index 0000000000..46ad5bde9c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php @@ -0,0 +1,72 @@ + $i + */ +function invalidParamClosureThisGeneric(callable $i) { + +} + +/** + * @param-closure-this FooBar $i + */ +function invalidParamClosureThisWrongGenericParams(callable $i) { + +} + +/** + * @param-closure-this FooBar $i + */ +function invalidParamClosureThisNotAllGenericParams(callable $i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-out.php b/tests/PHPStan/Rules/PhpDoc/data/param-out.php new file mode 100644 index 0000000000..5c4da69694 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-out.php @@ -0,0 +1,115 @@ + $i + */ +function unresolvableParamOutType(int &$i) { + +} + +/** + * @param-out \Exception $i + */ +function invalidParamOutGeneric(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutWrongGenericParams(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutNotAllGenericParams(int &$i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class C { + /** + * @var \Closure|null + */ + private $onCancel; + + public function __construct() { + $this->foo($this->onCancel); + } + + /** + * @param mixed $onCancel + * @param-out \Closure $onCancel + */ + public function foo(&$onCancel) : void { + $onCancel = function (): void {}; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php 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-empty-array-widening.php b/tests/PHPStan/Rules/PhpDoc/data/var-above-empty-array-widening.php new file mode 100644 index 0000000000..2664d8968e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-above-empty-array-widening.php @@ -0,0 +1,29 @@ + $a */ +$a = []; + +/** @var array{string, int} $a */ +$a = []; + +/** @var int $a */ +$a = []; + +$translationsTree = []; + +/** @var array $byRef */ +$byRef = &$translationsTree; diff --git a/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php new file mode 100644 index 0000000000..d8d72956fc --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php @@ -0,0 +1,78 @@ +foo; + } + + public function doBar() + { + /** @var string */ + return $this->foo; + } + +} + +class Baz +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + return $this->doBar($foo); + } + + public function doBar(int $foo) + { + /** @var string $foo */ + return $this->doFoo($foo); + } + +} + +class Lorem +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + if ($foo) { + + } + } + + public function doBar(int $foo) + { + /** @var string $foo */ + if ($foo) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-var-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 ceffc3b89c..30f7dd3182 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php @@ -79,16 +79,16 @@ public function doBaz() static $var; /** @var int */ - static $var; + static $var2; /** @var int */ - static $var, $bar; + static $var3, $bar; /** * @var int * @var string */ - static $var, $bar; + static $var4, $bar2; /** @var int $foo */ static $test; @@ -115,6 +115,7 @@ public function multiplePrefixedTagsAreFine() * @var int * @phpstan-var int * @psalm-var int + * @phan-var int */ $test = doFoo(); // OK @@ -313,3 +314,20 @@ function doFoo(): void { } + +class VarTagAboveLiteralArray +{ + + public function doFoo(): void + { + /** @var array */ + $arr = ['' => 'empty', 1 => '1']; + } + + public function doFoo2(): void + { + /** @var array */ + $arr = ['' => 'empty', 1 => '1']; + } + +} diff --git a/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php new file mode 100644 index 0000000000..789c48ccfb --- /dev/null +++ b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php @@ -0,0 +1,39 @@ + + */ +class FunctionNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FunctionNeverRule(new NeverRuleHelper()); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-never.php'], [ + [ + 'Function FunctionNever\doBar() always throws an exception, it should have return type "never".', + 18, + ], + [ + 'Function FunctionNever\callsNever() always terminates script execution, it should have return type "never".', + 23, + ], + [ + 'Function FunctionNever\doBaz() always terminates script execution, it should have return type "never".', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php new file mode 100644 index 0000000000..35b3d20463 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php @@ -0,0 +1,39 @@ + + */ +class MethodNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodNeverRule(new NeverRuleHelper()); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-never.php'], [ + [ + 'Method MethodNever\Foo::doBar() always throws an exception, it should have return type "never".', + 21, + ], + [ + 'Method MethodNever\Foo::callsNever() always terminates script execution, it should have return type "never".', + 26, + ], + [ + 'Method MethodNever\Foo::doBaz() always terminates script execution, it should have return type "never".', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php new file mode 100644 index 0000000000..146c26325e --- /dev/null +++ b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php @@ -0,0 +1,34 @@ + + */ +class NoPhpCodeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoPhpCodeRule(); + } + + public function testEmptyFile(): void + { + $this->analyse([__DIR__ . '/data/empty.php'], []); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/no-php-code.php'], [ + [ + 'The example does not contain any PHP code. Did you forget the opening + */ +class PhpdocCommentRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PhpdocCommentRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/comments.php'], [ + [ + 'Comment contains PHPDoc tag but does not start with /** prefix.', + 13, + ], + [ + 'Comment contains PHPDoc tag but does not start with /** prefix.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php b/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php new file mode 100644 index 0000000000..40f16771aa --- /dev/null +++ b/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php @@ -0,0 +1,42 @@ +> + */ +class PromoteParameterRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PromoteParameterRule( + new UninitializedPropertyRule(new ConstructorsHelper( + self::getContainer(), + [], + )), + 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/PromoteParameterRuleWithOriginalRuleTest.php b/tests/PHPStan/Rules/Playground/PromoteParameterRuleWithOriginalRuleTest.php new file mode 100644 index 0000000000..4495def574 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/PromoteParameterRuleWithOriginalRuleTest.php @@ -0,0 +1,53 @@ +> + */ +class PromoteParameterRuleWithOriginalRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PromoteParameterRule( + new OverridingMethodRule( + self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(MethodSignatureRule::class), + true, + self::getContainer()->getByType(MethodParameterComparisonHelper::class), + self::getContainer()->getByType(MethodVisibilityComparisonHelper::class), + self::getContainer()->getByType(MethodPrototypeFinder::class), + true, + ), + self::getContainer(), + InClassMethodNode::class, + false, + 'checkMissingOverrideMethodAttribute', + ); + } + + #[RequiresPhp('>= 8.3')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/promote-missing-override.php'], [ + [ + 'Method PromoteMissingOverride\Bar::doFoo() overrides method PromoteMissingOverride\Foo::doFoo() but is missing the #[\Override] attribute.', + 18, + 'This error would be reported if the checkMissingOverrideMethodAttribute: 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/comments.php b/tests/PHPStan/Rules/Playground/data/comments.php new file mode 100644 index 0000000000..83f67ba589 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/comments.php @@ -0,0 +1,49 @@ +foo = $foo; } + + /* + * @return T + */ + public function getFoo(): FooInterface + { + return $this->foo; + } + + /* + * some method + */ + public function getBar(): FooInterface + { + return $this->foo; + } + + // this should not error: @var + # this should not error: @var + + /* + * comments which look like phpdoc should be ignored + * + * x@x.cz + * 10 amps @ 1 volt + */ + public function ignoreComments(): FooInterface + { + return $this->foo; + } +} 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-missing-override.php b/tests/PHPStan/Rules/Playground/data/promote-missing-override.php new file mode 100644 index 0000000000..353d3f9cd2 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/promote-missing-override.php @@ -0,0 +1,34 @@ += 8.3 + +namespace PromoteMissingOverride; + +class Foo +{ + + public function doFoo(): void + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): void + { + + } + + + public function doBar(): void + { + + } + + #[\Override] + public function doBaz(): void + { + } + +} 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 @@ + + * @extends RuleTestCase */ class AccessPropertiesInAssignRuleTest extends RuleTestCase { protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true) + new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true, 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 testDynamicStringableAccess(): void + { + // All warnings are reported by the AccessPropertiesRule. + // The AccessPropertiesInAssignRule does not report any warnings. + $this->analyse([__DIR__ . '/data/dynamic-stringable-access.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testDynamicStringableNullsafeAccess(): void + { + // All warnings are reported by the AccessPropertiesRule. + // The AccessPropertiesInAssignRule does not report any warnings. + $this->analyse([__DIR__ . '/data/dynamic-stringable-nullsafe-access.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 + { + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors = [ + [ + 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', + 27, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); + } + + public function testBug10477(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10477.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testAsymmetricVisibility(): void + { + $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, + ], + ]); + } + + public function testBug13123(): void + { + $this->analyse([__DIR__ . '/data/bug-13123.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 877e8e1fb0..85885856b0 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -2,177 +2,182 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +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 { - $reflectionProvider = $this->createReflectionProvider(); - return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); + $reflectionProvider = self::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, true)); } public function testAccessProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', - 23, + 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', - 24, + 25, ], [ 'Cannot access property $propertyOnString on string.', - 31, + 32, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 42, + 43, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 43, + 44, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', - 49, + 50, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', - 52, + 53, + $tipText, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 58, + 59, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 59, + 60, ], [ 'Access to property $foo on an unknown class TestAccessProperties\UnknownClass.', - 63, + 64, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', - 68, + 69, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', - 70, + 71, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 76, + 77, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 77, + 78, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 80, + 81, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 83, + 84, + $tipText, ], [ 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $test on an unknown class TestAccessProperties\SecondUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\WithFooAndBarProperty|TestAccessProperties\WithFooProperty::$bar.', - 176, + 177, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', - 193, + 194, + $tipText, ], [ 'Cannot access property $ipsum on TestAccessProperties\FooAccessProperties|null.', - 207, + 208, ], [ 'Cannot access property $foo on null.', - 220, + 221, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', - 247, + 248, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', - 250, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 264, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 266, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 270, + 251, + $tipText, ], [ 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', - 272, + 274, ], [ 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', - 272, + 274, ], [ 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', - 272, - ], - [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 274, ], [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], - [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', + 302, + $tipText, ], [ 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', - 402, + 407, ], [ - 'Cannot access property $array on stdClass|null.', - 412, + 'Access to an undefined property object::$baz.', + 438, + $tipText, ], - ] + ], ); } @@ -180,163 +185,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, - ], - [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 274, ], [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, + '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\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.', @@ -349,10 +376,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.', @@ -365,10 +394,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, ], ]); } @@ -377,10 +408,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, ], ]); } @@ -389,6 +424,7 @@ public function testClassExists(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-class-exists.php'], [ [ @@ -418,10 +454,14 @@ public function testMixin(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/mixin.php'], [ [ 'Access to an undefined property MixinProperties\GenericFoo::$namee.', - 51, + 55, + $tipText, ], ]); } @@ -430,22 +470,22 @@ public function testBug3947(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3947.php'], []); } public function testNullSafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/nullsafe-property-fetch.php'], [ [ 'Access to an undefined property NullsafePropertyFetch\Foo::$baz.', 13, + $tipText, ], [ 'Cannot access property $bar on string.', @@ -463,6 +503,14 @@ public function testNullSafe(): void 'Cannot access property $bar on string.', 22, ], + [ + 'Cannot access property $foo on null.', + 28, + ], + [ + 'Cannot access property $foo on null.', + 29, + ], ]); } @@ -470,17 +518,15 @@ public function testBug3371(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3371.php'], []); } public function testBug4527(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4527.php'], []); } @@ -488,7 +534,708 @@ public function testBug4808(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4808.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug5868(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-5868.php'], [ + [ + 'Cannot access property $child on Bug5868PropertyFetch\Foo|null.', + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6385(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/bug-6385.php'], [ + [ + 'Access to an undefined property UnitEnum::$value.', + 43, + $tipText, + ], + [ + 'Access to an undefined property Bug6385\ActualUnitEnum::$value.', + 47, + $tipText, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6566(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-6566.php'], []); + } + + public function testBug6899(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $errors = [ + [ + 'Cannot access property $prop on string.', + 13, + ], + [ + 'Cannot access property $prop on string.', + 14, + ], + [ + 'Cannot access property $prop on string.', + 15, + ], + ]; + $this->analyse([__DIR__ . '/data/bug-6899.php'], $errors); + } + + public function testBug6026(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-6026.php'], []); + } + + public function testBug3659(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $errors = []; + $this->analyse([__DIR__ . '/data/bug-3659.php'], $errors); + } + + public static 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)], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataDynamicProperties')] + 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 static 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, + ]; + if ($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, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\ReadonlyWithMagic::$foo.', + 133, + $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, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\ReadonlyWithMagic::$foo.', + 133, + $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; + + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors = [ + [ + 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', + 26, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8536(): void + { + $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 testDynamicStringableAccess(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/dynamic-stringable-access.php'], [ + // DynamicStringableAccess\Foo::testProperties() + [ + 'Property name for $this(DynamicStringableAccess\Foo) must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 13, + ], + [ + 'Property name for DynamicStringableAccess\Foo must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 14, + ], + [ + 'Property name for $this(DynamicStringableAccess\Foo) must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 15, + ], + [ + 'Property name for $this(DynamicStringableAccess\Foo) must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 16, + ], + [ + 'Property name for $this(DynamicStringableAccess\Foo) must be a string, but array was given.', + 18, + ], + // DynamicStringableAccess\Foo::testPropertyAssignments() + [ + 'Property name for $this(DynamicStringableAccess\Foo) must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 30, + ], + [ + 'Property name for DynamicStringableAccess\Foo must be a string, but $this(DynamicStringableAccess\Foo) was given.', + 31, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testDynamicStringableNullsafeAccess(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/dynamic-stringable-nullsafe-access.php'], [ + // DynamicStringableNullsafeAccess\Foo::testNullsafePropertyFetch() + [ + 'Property name for $this(DynamicStringableNullsafeAccess\Foo) must be a string, but $this(DynamicStringableNullsafeAccess\Foo) was given.', + 13, + ], + [ + 'Property name for DynamicStringableNullsafeAccess\Foo must be a string, but $this(DynamicStringableNullsafeAccess\Foo) was given.', + 14, + ], + [ + 'Property name for $this(DynamicStringableNullsafeAccess\Foo) must be a string, but $this(DynamicStringableNullsafeAccess\Foo) was given.', + 15, + ], + [ + 'Property name for $this(DynamicStringableNullsafeAccess\Foo) must be a string, but $this(DynamicStringableNullsafeAccess\Foo) was given.', + 16, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9694(): void + { + $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 testBug9706(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-9706.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testAsymmetricVisibility(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/read-asymmetric-visibility.php'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testNewIsAlwaysFinalClass(): void + { + $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'], []); + } + + public function testDiscussion13274(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/discussion-13274.php'], []); + } + + public function testBug13271(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-13271.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11424(): 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/bug-11424.php'], [ + [ + 'Access to an undefined property object{hello?: string}::$hello.', + 10, + $tipText, + ], + [ + 'Access to an undefined property Bug11424\Bar|Bug11424\Foo::$i.', + 31, + $tipText, + ], + ]); + } + + public function testBug12645(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-12645.php'], [ + [ + 'Access to private property Bug12645\Foo::$id.', + 18, + ], + [ + 'Access to private property Bug12645\Foo::$id.', + 19, + ], + [ + 'Access to private property Bug12645\Foo::$id.', + 24, + ], + ]); + } + + public function testBug11289(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-11289.php'], []); + } + + public function testBug8668(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-8668.php'], [ + [ + 'Non-static access to static property Bug8668\Sample::$sample.', + 9, + ], + [ + 'Non-static access to static property Bug8668\Sample::$sample.', + 10, + ], + [ + 'Non-static access to static property Bug8668\Sample2::$sample.', + 20, + ], + [ + 'Non-static access to static property Bug8668\Sample2::$sample.', + 21, + ], + ]); + } + + public function testPrivatePropertyWithAllowedPropertyTagIsPublic(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/private-property-with-allowed-property-tag-is-public.php'], []); + } + + public function testBug13537(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors = [ + [ + 'Cannot access property $bob on array.', + 26, + ], + [ + 'Access to protected property Bug13537\Bar::$test.', + 26, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-13537.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index 2aa987e50e..01b3133fa6 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 { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), new ClassCaseSensitivityCheck($reflectionProvider)) + 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,6 +38,20 @@ 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, ], ]); diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 9409c97014..f15d03c7ad 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 { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AccessStaticPropertiesRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false), - new ClassCaseSensitivityCheck($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, @@ -56,22 +67,50 @@ public function testAccessStaticProperties(): void 47, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', + 49, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', + 52, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', 53, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$nonexistent.', + 54, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$nonexistent.', 55, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyBaz.', + 60, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyBaz.', 63, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyNonexistent.', + 64, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyNonexistent.', 65, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 70, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 71, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', 71, @@ -80,10 +119,34 @@ public function testAccessStaticProperties(): void 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', 72, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 72, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 73, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', 75, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 75, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 76, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 77, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 78, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', 78, @@ -157,13 +220,17 @@ 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.', @@ -182,6 +249,32 @@ public function testAccessStaticProperties(): void 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', 224, ], + [ + 'Access to an undefined static property DoesNotAllowDynamicProperties::$foo.', + 234, + ], + [ + 'Access to an undefined static property AllowsDynamicProperties::$foo.', + 248, + ], + [ + 'Static access to instance property ParentClassWithInstanceProperty::$i.', + 267, + ], + [ + 'Access to an undefined static property ParentClassWithInstanceProperty::$j.', + 268, + ], + ]); + } + + public function testRuleAssignOp(): void + { + $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ + [ + 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', + 10, + ], ]); } @@ -195,4 +288,55 @@ 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, + ], + ]); + } + + public function testBug12775(): void + { + $this->analyse([__DIR__ . '/data/bug-12775.php'], []); + } + + public function testBug8668Bis(): void + { + $this->analyse([__DIR__ . '/data/bug-8668-bis.php'], [ + [ + 'Static access to instance property Bug8668Bis\Sample::$sample.', + 9, + ], + [ + 'Static access to instance property Bug8668Bis\Sample::$sample.', + 10, + ], + [ + 'Static access to instance property Bug8668Bis\Sample2::$sample.', + 20, + ], + [ + 'Static access to instance property Bug8668Bis\Sample2::$sample.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/Bug7074Test.php b/tests/PHPStan/Rules/Properties/Bug7074Test.php new file mode 100644 index 0000000000..8e1ef0c3e0 --- /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(self::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 42064cd3e8..1bc6512618 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(self::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,22 +45,24 @@ public function testDefaultValueForNativePropertyType(): void ]); } - public function testDefaultValueForPromotedProperty(): void + public function testBug5607(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - - $this->analyse([__DIR__ . '/data/default-value-for-promoted-property.php'], [ - [ - 'Property DefaultValueForPromotedProperty\Foo::$foo (int) does not accept default value of type string.', - 9, - ], + $this->analyse([__DIR__ . '/data/bug-5607.php'], [ [ - 'Property DefaultValueForPromotedProperty\Foo::$foo (int) does not accept default value of type string.', + 'Property Bug5607\Cl::$u (Bug5607\A|null) does not accept default value of type array.', 10, ], ]); } + 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/DirectReadWritePropertiesExtensionProvider.php b/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php deleted file mode 100644 index bcfe73709f..0000000000 --- a/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php +++ /dev/null @@ -1,27 +0,0 @@ -extensions = $extensions; - } - - /** - * @return ReadWritePropertiesExtension[] - */ - public function getExtensions(): array - { - return $this->extensions; - } - -} diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index 99dea64d96..ac30206c91 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -2,23 +2,40 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +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 = self::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 ); } @@ -40,22 +57,22 @@ public function testNonexistentClass(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Dolor as its type.', + 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Ipsum as its type.', 21, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Ipsum as its type.', + 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Dolor as its type.', 21, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\BAR as its type.', + 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\Fooo as its type.', 24, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\Fooo as its type.', + 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\BAR as its type.', 24, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -67,6 +84,10 @@ public function testNonexistentClass(): void 'Property PropertiesTypes\Foo::$withTrait has invalid type PropertiesTypes\SomeTrait.', 27, ], + [ + 'Class DateTime referenced with incorrect case: Datetime.', + 30, + ], [ 'Property PropertiesTypes\Foo::$nonexistentClassInGenericObjectType has unknown class PropertiesTypes\Foooo as its type.', 33, @@ -77,16 +98,12 @@ public function testNonexistentClass(): void 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.', @@ -108,10 +125,6 @@ public function testNativeTypes(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/properties-promoted-types.php'], [ [ 'Property PromotedPropertiesExistingClasses\Foo::$baz has invalid type PromotedPropertiesExistingClasses\SomeTrait.', @@ -134,4 +147,35 @@ public function testPromotedProperties(): void ]); } + public static function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Property PropertyIntersectionTypes\Test::$prop2 has unresolvable native type.', + 30, + ], + [ + 'Property PropertyIntersectionTypes\Test::$prop3 has unresolvable native type.', + 32, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataIntersectionTypes')] + 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..05610e0482 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php @@ -0,0 +1,65 @@ + + */ +class ExistingClassesInPropertyHookTypehintsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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, + ), + ); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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..e00ba2dd3f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/GetNonVirtualPropertyHookReadRuleTest.php @@ -0,0 +1,41 @@ + + */ +class GetNonVirtualPropertyHookReadRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new GetNonVirtualPropertyHookReadRule(); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testAbstractProperty(): void + { + $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 46fbc40179..c274aff53c 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -3,17 +3,18 @@ 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, true)); + return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -34,21 +35,28 @@ public function testRule(): void [ 'Property MissingPropertyTypehint\ChildClass::$unionProp type has no value type specified in iterable type array.', 32, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Property MissingPropertyTypehint\Bar::$foo with generic interface MissingPropertyTypehint\GenericInterface does not specify its types: T, U', - 74, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 77, ], [ 'Property MissingPropertyTypehint\Bar::$baz with generic class MissingPropertyTypehint\GenericClass does not specify its types: A, B', - 80, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 83, ], [ 'Property MissingPropertyTypehint\CallableSignature::$cb type has no signature specified for callable.', - 93, + 96, + ], + [ + 'Property MissingPropertyTypehint\NestedArrayInProperty::$args type has no value type specified in iterable type array.', + 106, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Property MissingPropertyTypehint\Baz::$bar with generic class MissingPropertyTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 134, ], ]); } @@ -58,22 +66,14 @@ public function testBug3402(): void $this->analyse([__DIR__ . '/data/bug-3402.php'], []); } + public function testBug11761(): void + { + $this->analyse([__DIR__ . '/data/bug-11761-bis.php'], []); + } + public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], [ - [ - 'Property PromotedPropertiesMissingTypehint\Foo::$lorem has no type specified.', - 15, - ], - [ - 'Property PromotedPropertiesMissingTypehint\Foo::$ipsum type has no value type specified in iterable type array.', - 16, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, - ], - ]); + $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], []); } } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php new file mode 100644 index 0000000000..b503c69e45 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -0,0 +1,152 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleIgnoresNativeReadonly(): void + { + $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..6d1772cc75 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,354 @@ + + */ +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; + } + + }, + ]; + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7119(): void + { + $this->analyse([__DIR__ . '/data/bug-7119.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7314(): void + { + $this->analyse([__DIR__ . '/data/bug-7314.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8412(): void + { + $this->analyse([__DIR__ . '/data/bug-8412.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8958(): void + { + $this->analyse([__DIR__ . '/data/bug-8958.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8563(): void + { + $this->analyse([__DIR__ . '/data/bug-8563.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6402(): void + { + $this->analyse([__DIR__ . '/data/bug-6402.php'], [ + [ + 'Access to an uninitialized readonly property Bug6402\SomeModel2::$views.', + 28, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7198(): void + { + $this->analyse([__DIR__ . '/data/bug-7198.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7649(): void + { + $this->analyse([__DIR__ . '/data/bug-7649.php'], [ + [ + 'Class Bug7649\Foo has an uninitialized readonly property $bar. Assign it in the constructor.', + 7, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9577(): void + { + $this->analyse([__DIR__ . '/../Classes/data/bug-9577.php'], [ + [ + 'Class Bug9577\SpecializedException2 has an uninitialized readonly property $message. Assign it in the constructor.', + 8, + ], + ]); + } + + #[RequiresPhp('>= 8.3')] + public function testAnonymousReadonlyClass(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10523(): void + { + $this->analyse([__DIR__ . '/data/bug-10523.php'], [ + [ + 'Readonly property Bug10523\MultipleWrites::$userAccount is already assigned.', + 55, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10822(): void + { + $this->analyse([__DIR__ . '/data/bug-10822.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testRedeclaredReadonlyProperties(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.2')] + public function testRedeclaredPropertiesOfReadonlyClass(): void + { + $this->analyse([__DIR__ . '/data/redeclare-property-of-readonly-class.php'], [ + [ + 'Readonly property RedeclarePropertyOfReadonlyClass\B1::$promotedProp is already assigned.', + 15, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8101(): void + { + $this->analyse([__DIR__ . '/data/bug-8101.php'], [ + [ + 'Readonly property Bug8101\B::$myProp is already assigned.', + 12, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9863(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10048(): void + { + $this->shouldNarrowMethodScopeFromConstructor = true; + $this->analyse([__DIR__ . '/data/bug-10048.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11828(): void + { + $this->shouldNarrowMethodScopeFromConstructor = true; + $this->analyse([__DIR__ . '/data/bug-11828.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9864(): void + { + $this->analyse([__DIR__ . '/data/bug-9864.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php index 5ed217dd66..259f973c22 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -18,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-rule.php'], [ [ 'Using nullsafe property access on non-nullable type Exception. Use -> instead.', @@ -30,4 +27,44 @@ public function testRule(): void ]); } + public function testBug6020(): void + { + $this->analyse([__DIR__ . '/data/bug-6020.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7109(): void + { + $this->analyse([__DIR__ . '/data/bug-7109.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug5172(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5172.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7980(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-7980.php'], []); + } + + public function testBug8517(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8517.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug9105(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9105.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6922(): void + { + $this->analyse([__DIR__ . '/data/bug-6922.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index a3e5962157..91fed911ba 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function sprintf; /** * @extends RuleTestCase @@ -11,20 +15,19 @@ class OverridingPropertyRuleTest extends RuleTestCase { - /** @var bool */ - private $reportMaybes; + private bool $reportMaybes; protected function getRule(): Rule { - return new OverridingPropertyRule(true, $this->reportMaybes); + return new OverridingPropertyRule( + self::getContainer()->getByType(PhpVersion::class), + true, + $this->reportMaybes, + ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->reportMaybes = true; $this->analyse([__DIR__ . '/data/overriding-property.php'], [ [ @@ -96,22 +99,22 @@ public function testRule(): void 158, sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s", - 'https://phpstan.org/user-guide/stub-files' + 'https://phpstan.org/user-guide/stub-files', ), ], ]); } - public function dataRulePHPDocTypes(): array + public static function dataRulePHPDocTypes(): array { $tip = sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s", - 'https://phpstan.org/user-guide/stub-files' + '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%.' + 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', ); return [ @@ -154,14 +157,130 @@ public function dataRulePHPDocTypes(): array } /** - * @dataProvider dataRulePHPDocTypes - * @param bool $reportMaybes - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRulePHPDocTypes')] 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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testFinal(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/overriding-final-property.php'], [ + [ + 'Property OverridingFinalProperty\Bar::$a overrides final property OverridingFinalProperty\Foo::$a.', + 27, + ], + [ + 'Property OverridingFinalProperty\Bar::$b overrides final property OverridingFinalProperty\Foo::$b.', + 29, + ], + [ + 'Property OverridingFinalProperty\Bar::$c overrides final property OverridingFinalProperty\Foo::$c.', + 31, + ], + [ + 'Property OverridingFinalProperty\Bar::$d overrides final property OverridingFinalProperty\Foo::$d.', + 33, + ], + [ + 'Property OverridingFinalProperty\Bar::$e overrides @final property OverridingFinalProperty\Foo::$e.', + 35, + ], + [ + 'Property OverridingFinalProperty\Bar::$f overrides @final property OverridingFinalProperty\Foo::$f.', + 37, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyPrototypeFromInterface(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12466(): void + { + $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 testBug11761(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-11761.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12586(): void + { + $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..3edbe05c09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -0,0 +1,193 @@ + + */ +class PropertiesInInterfaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertiesInInterfaceRule(new PhpVersion(PHP_VERSION_ID)); + } + + #[RequiresPhp('< 8.4')] + public function testPhp83AndPropertiesInInterface(): void + { + // @phpstan-ignore phpstan.skipTestsRequiresPhp + 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, + ], + ]); + } + + #[RequiresPhp('< 8.4')] + public function testPhp83AndPropertyHooksInInterface(): void + { + // @phpstan-ignore phpstan.skipTestsRequiresPhp + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndPropertiesInInterface(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndNonPublicPropertyHooksInInterface(): void + { + $this->analyse([__DIR__ . '/data/property-hooks-visibility-in-interface.php'], [ + [ + 'Interfaces cannot include non-public properties.', + 7, + ], + [ + 'Interfaces cannot include non-public properties.', + 9, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndPropertyHooksWithBodiesInInterface(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndReadonlyPropertyHooksInInterface(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndFinalPropertyHooksInInterface(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndExplicitAbstractProperty(): void + { + $this->analyse([__DIR__ . '/data/property-in-interface-explicit-abstract.php'], [ + [ + 'Property in interface cannot be explicitly abstract.', + 8, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndStaticHookedPropertyInInterface(): void + { + $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..2759afcfed --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyAssignRefRuleTest.php @@ -0,0 +1,64 @@ + + */ +class PropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyAssignRefRule(new PhpVersion(PHP_VERSION_ID), new PropertyReflectionFinder()); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testAsymmetricVisibility(): void + { + $this->analyse([__DIR__ . '/data/property-assign-ref-asymmetric.php'], [ + [ + 'Property PropertyAssignRefAsymmetric\Foo::$a with private(set) visibility is assigned by reference.', + 28, + ], + [ + 'Property PropertyAssignRefAsymmetric\Foo::$a with private(set) visibility is assigned by reference.', + 36, + ], + [ + 'Property PropertyAssignRefAsymmetric\Foo::$b with protected(set) visibility is assigned by reference.', + 37, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 92b41031aa..e5eab80521 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -20,31 +21,33 @@ class PropertyAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new PropertyAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, - true + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + true, + ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/property-attributes.php'], [ [ 'Attribute class PropertyAttributes\Foo does not have the property target.', @@ -53,4 +56,18 @@ public function testRule(): void ]); } + public function testDeprecatedAttribute(): void + { + $this->analyse([__DIR__ . '/data/property-attributes-deprecated.php'], [ + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWay is deprecated.', + 16, + ], + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWayWithDescription is deprecated: Use something else please', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php new file mode 100644 index 0000000000..e59a22028d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -0,0 +1,72 @@ + + */ +class PropertyHookAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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, + ), + ); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-hook-attributes.php'], [ + [ + 'Attribute class PropertyHookAttributes\Foo does not have the method target.', + 27, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testNoDiscard(): void + { + $this->analyse([__DIR__ . '/data/property-hook-attributes-nodiscard.php'], [ + [ + 'Attribute class NoDiscard cannot be used on property hooks.', + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php new file mode 100644 index 0000000000..d04cdb6667 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php @@ -0,0 +1,284 @@ + + */ +class PropertyInClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyInClassRule(new PhpVersion(PHP_VERSION_ID)); + } + + #[RequiresPhp('< 8.4')] + public function testPhpLessThan84AndHookedPropertiesInClass(): void + { + // @phpstan-ignore phpstan.skipTestsRequiresPhp + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndHookedPropertiesWithoutBodiesInClass(): void + { + // @phpstan-ignore phpstan.skipTestsRequiresPhp + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndNonAbstractHookedPropertiesInClass(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractHookedPropertiesInClass(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndNonAbstractHookedPropertiesInAbstractClass(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractNonHookedPropertiesInAbstractClass(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractHookedPropertiesWithBodies(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndReadonlyHookedProperties(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndVirtualHookedProperties(): void + { + $this->analyse([__DIR__ . '/data/virtual-hooked-properties.php'], [ + [ + 'Virtual hooked properties cannot have a default value.', + 17, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndStaticHookedProperties(): void + { + $this->analyse([__DIR__ . '/data/static-hooked-properties.php'], [ + [ + 'Hooked properties cannot be static.', + 7, + ], + [ + 'Hooked properties cannot be static.', + 15, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndPrivateFinalHookedProperties(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractFinalHookedProperties(): void + { + $this->analyse([__DIR__ . '/data/abstract-final-property-hook.php'], [ + [ + 'Property cannot be both abstract and final.', + 7, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractPrivateHookedProperties(): void + { + $this->analyse([__DIR__ . '/data/abstract-private-property-hook.php'], [ + [ + 'Property cannot be both abstract and private.', + 7, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84AndAbstractFinalHookedPropertiesParseError(): void + { + // 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84FinalProperties(): void + { + $this->analyse([__DIR__ . '/data/final-properties.php'], [ + [ + 'Property cannot be both final and private.', + 7, + ], + ]); + } + + #[RequiresPhp('< 8.4')] + public function testBeforePhp84FinalProperties(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPhp84FinalPropertyHooks(): void + { + $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..5a228e1bb0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -0,0 +1,68 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleIgnoresNativeReadonly(): void + { + $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..adbd30eb43 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -0,0 +1,188 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleIgnoresNativeReadonly(): void + { + $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc-and-native.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7361(): void + { + $this->analyse([__DIR__ . '/data/bug-7361.php'], [ + [ + '@readonly property Bug7361\Example::$foo is assigned outside of the constructor.', + 12, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testFeature7648(): void + { + $this->analyse([__DIR__ . '/data/feature-7648.php'], []); + } + + #[RequiresPhp('>= 7.4')] + public function testFeature11775(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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..13d3d988aa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ReadOnlyByPhpDocPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyByPhpDocPropertyRule(); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleIgnoresNativeReadonly(): void + { + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-and-native.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testRuleAllowedPrivateMutation(): void + { + $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..4e0d4b4293 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ReadOnlyPropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRefRule(new PropertyReflectionFinder()); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..f5a471f6eb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,177 @@ + + */ +class ReadOnlyPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRule( + new PropertyReflectionFinder(), + new ConstructorsHelper( + self::getContainer(), + [ + 'ReadonlyPropertyAssign\\TestCase::setUp', + ], + ), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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); + } + + #[RequiresPhp('>= 8.1')] + public function testFeature7648(): void + { + $this->analyse([__DIR__ . '/data/feature-7648.php'], [ + [ + 'Readonly property Feature7648\Request::$offset is assigned outside of the constructor.', + 23, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testReadOnlyClasses(): void + { + $this->analyse([__DIR__ . '/data/readonly-class-assign.php'], [ + [ + 'Readonly property ReadonlyClassPropertyAssign\Foo::$foo is assigned outside of the constructor.', + 21, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug6773(): void + { + $this->analyse([__DIR__ . '/data/bug-6773.php'], [ + [ + 'Readonly property Bug6773\Repository::$data is assigned outside of the constructor.', + 16, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug8929(): void + { + $this->analyse([__DIR__ . '/data/bug-8929.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12537(): void + { + $this->analyse([__DIR__ . '/data/bug-12537.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php index 7bcd73b7c9..1f51f7ad26 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -12,15 +13,14 @@ class ReadOnlyPropertyRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId; + private int $phpVersionId; protected function getRule(): Rule { return new ReadOnlyPropertyRule(new PhpVersion($this->phpVersionId)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -46,6 +46,18 @@ public function dataRule(): array '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, + ], ], ], [ @@ -59,24 +71,33 @@ public function dataRule(): array 'Readonly property cannot have a default value.', 10, ], + [ + 'Readonly property cannot be static.', + 23, + ], ], ], ]; } /** - * @dataProvider dataRule - * @param int $phpVersionId - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/read-only-property.php'], $errors); } + /** + * @param list $errors + */ + #[DataProvider('dataRule')] + 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 f496d70757..cc9392f19f 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::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,22 +67,51 @@ public function testReadingWriteOnlyPropertiesCheckThisOnly(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 17, + 23, ], ]); } public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/reading-write-only-properties-nullsafe.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 9, + 10, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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, + ], + ]); + } + + public function testPrivatePropertyTagWrite(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/private-property-tag-write.php'], [ + [ + 'Property PrivatePropertyTagWrite\Foo::$foo is not readable.', + 22, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php b/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php new file mode 100644 index 0000000000..e4c9a0bb7f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php @@ -0,0 +1,35 @@ + + */ +class SetNonVirtualPropertyHookAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new SetNonVirtualPropertyHookAssignRule(); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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..588845b5a1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php @@ -0,0 +1,65 @@ + + */ +class SetPropertyHookParameterRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new SetPropertyHookParameterRule(new MissingTypehintCheck(true, []), true, true); + } + + #[RequiresPhp('>= 8.4')] + public function testRule(): void + { + $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 c50283c221..4a12d0636a 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -2,17 +2,24 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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; + + private bool $checkImplicitMixed = 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(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -38,10 +45,6 @@ public function testTypesAssignedToProperties(): void 'Static property PropertiesAssignedTypes\Foo::$staticStringProperty (string) does not accept int.', 37, ], - [ - 'Property PropertiesAssignedTypes\Ipsum::$parentStringProperty (string) does not accept int.', - 39, - ], [ 'Property PropertiesAssignedTypes\Foo::$unionPropertySelf (array|(iterable&PropertiesAssignedTypes\Collection)) does not accept PropertiesAssignedTypes\Foo.', 44, @@ -78,6 +81,45 @@ public function testTypesAssignedToProperties(): void 'Property PropertiesAssignedTypes\AssignRefFoo::$stringProperty (string) does not accept int.', 312, ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 334, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 335, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 346, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 347, + ], + [ + 'Property PropertiesAssignedTypes\ListAssign::$foo (string) does not accept int.', + 360, + ], + [ + 'Property PropertiesAssignedTypes\AppendToArrayAccess::$collection2 (ArrayAccess&Countable) does not accept Countable.', + 376, + ], + [ + '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.', + ], ]); } @@ -86,11 +128,15 @@ public function testBug1216(): void $this->analyse([__DIR__ . '/data/bug-1216.php'], [ [ 'Property Bug1216PropertyTest\Baz::$untypedBar (string) does not accept int.', - 35, + 38, + ], + [ + 'Property Bug1216PropertyTest\Baz::$untypedBar (string) does not accept int.', + 46, ], [ 'Property Bug1216PropertyTest\Dummy::$foo (Exception) does not accept stdClass.', - 59, + 68, ], ]); } @@ -111,21 +157,9 @@ public function testTypesAssignedToPropertiesExpressionNames(): void 66, ], [ - 'Property PropertiesFromArrayIntoObject\Foo::$float_test (float) does not accept float|int|string.', - 69, - ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.', - 69, - ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept float|int|string.', + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', 69, ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept (float|int).', - 73, - ], [ 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.', 83, @@ -187,6 +221,725 @@ public function testBug3777(): void 168, ], ]); + + $this->analyse([__DIR__ . '/data/bug-3777-static.php'], [ + [ + 'Static property Bug3777Static\Bar::$foo (Bug3777Static\Foo) does not accept Bug3777Static\Fooo.', + 58, + ], + [ + 'Static property Bug3777Static\Ipsum::$ipsum (Bug3777Static\Lorem) does not accept Bug3777Static\Lorem.', + 95, + ], + [ + 'Static property Bug3777Static\Ipsum2::$lorem2 (Bug3777Static\Lorem2) does not accept Bug3777Static\Lorem2.', + 129, + ], + [ + 'Static property Bug3777Static\Ipsum2::$ipsum2 (Bug3777Static\Lorem2) does not accept Bug3777Static\Lorem2.', + 131, + ], + [ + 'Static property Bug3777Static\Ipsum3::$ipsum3 (Bug3777Static\Lorem3) does not accept Bug3777Static\Lorem3.', + 168, + ], + ]); + } + + public function testAppendendArrayKey(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/appended-array-key.php'], [ + [ + 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', + 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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug11275(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12565(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12565.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testShortBodySetHook(): void + { + $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.', + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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, + ], + ]); + } + + public function testBug13093c(): void + { + $this->analyse([__DIR__ . '/data/bug-13093c.php'], []); + } + + public function testBug13093d(): void + { + $this->analyse([__DIR__ . '/data/bug-13093d.php'], []); + } + + public function testBug8825(): void + { + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8825.php'], []); + } + + public function testBug7844(): void + { + $this->analyse([__DIR__ . '/data/bug-7844.php'], []); + } + + public function testBug7844b(): void + { + $this->analyse([__DIR__ . '/data/bug-7844b.php'], []); + } + + public function testBug12675(): void + { + $this->analyse([__DIR__ . '/data/bug-12675.php'], []); + } + + public function testBug11171(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11171.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8282(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8282.php'], []); + } + + public function testBug7824(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7824.php'], []); + } + + public function testBug13438(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438.php'], [ + [ + 'Property Bug13438\Test::$queue (non-empty-list) does not accept list.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438\Test::$queue (non-empty-list) does not accept list.', + 26, + 'list might be empty.', + ], + ]); + } + + public function testBug13438b(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438b.php'], [ + [ + 'Property Bug13438b\Test::$queue (non-empty-list) does not accept list.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438b\Test::$queue (non-empty-list) does not accept list.', + 26, + 'list might be empty.', + ], + + ]); + } + + public function testBug13438c(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438c.php'], []); + } + + public function testBug13438d(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438d.php'], [ + [ + 'Property Bug13438d\Test::$queue (array{}) does not accept array{1}.', + 18, + ], + ]); + } + + public function testBug13438e(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438e.php'], [ + [ + 'Property Bug13438e\Test::$queue (array{}) does not accept array{1}.', + 18, + ], + ]); + } + + public function testBug13438f(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438f.php'], [ + [ + 'Property Bug13438f\Test::$queue (array>) does not accept non-empty-array>.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438f\Test::$queue (array>) does not accept non-empty-array>.', + 25, + 'list might be empty.', + ], + ]); + } + + public function testBug2888(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-2888.php'], [ + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 17, + ], + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 18, + ], + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 26, + ], + ]); + } + + public function testBug11241b(): void + { + $this->analyse([__DIR__ . '/data/bug-11241b.php'], [ + [ + 'Property Bug11241b\HelloWorld::$property1 (bool) does not accept string.', + 14, + ], + ]); + } + + public function testBug11777(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11777.php'], []); + } + + public function testBug13035(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13035.php'], []); } } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index b4b5937bfa..940dc461a3 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function strpos; /** * @extends RuleTestCase @@ -16,38 +18,73 @@ class UninitializedPropertyRuleTest extends RuleTestCase protected function getRule(): Rule { return new UninitializedPropertyRule( - new DirectReadWritePropertiesExtensionProvider([ - new class() implements ReadWritePropertiesExtension { + new ConstructorsHelper( + self::getContainer(), + [ + 'UninitializedProperty\\TestCase::setUp', + 'Bug9619\\AdminPresenter::startup', + 'Bug9619\\AdminPresenter2::startup', + 'Bug9619\\AdminPresenter3::startup', + 'Bug9619\\AdminPresenter3::startup2', + ], + ), + ); + } - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } + protected function getReadWritePropertiesExtensions(): array + { + return [ + new class() implements ReadWritePropertiesExtension { - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return false; - } + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; - } + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return false; + } - }, - ]), - [ - 'UninitializedProperty\\TestCase::setUp', - ] - ); + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; + } + + }, + + // bug-9619 + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@inject') !== false; + } + + }, + ]; } - public function testRule(): void + public static function getAdditionalConfigFiles(): array { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } + return [ + __DIR__ . '/uninitialized-property-rule.neon', + ]; + } + public function testRule(): void + { $this->analyse([__DIR__ . '/data/uninitialized-property.php'], [ [ 'Class UninitializedProperty\Foo has an uninitialized property $bar. Give it default value or assign it in the constructor.', @@ -69,16 +106,127 @@ public function testRule(): void 'Class UninitializedProperty\TestExtension has an uninitialized property $uninited. Give it default value or assign it in the constructor.', 122, ], + [ + 'Class UninitializedProperty\FooTraitClass has an uninitialized property $bar. Give it default value or assign it in the constructor.', + 157, + ], + [ + 'Class UninitializedProperty\FooTraitClass has an uninitialized property $baz. Give it default value or assign it in the constructor.', + 159, + ], + /*[ + 'Access to an uninitialized property UninitializedProperty\InitializedInPublicSetterNonFinalClass::$foo.', + 278, + ],*/ + [ + 'Class UninitializedProperty\SometimesInitializedInPrivateSetter has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 286, + ], + [ + 'Access to an uninitialized property UninitializedProperty\SometimesInitializedInPrivateSetter::$foo.', + 303, + ], + [ + 'Class UninitializedProperty\EarlyReturn has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 372, + ], ]); } public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/uninitialized-property-promoted.php'], []); } + public function testReadOnly(): void + { + // 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, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12336(): void + { + $this->analyse([__DIR__ . '/data/bug-12336.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12547(): void + { + $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 c5415f6c57..c46d35de67 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -2,20 +2,23 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +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(self::createReflectionProvider(), true, false, true, false, false, false, true), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); } public function testCheckThisOnlyProperties(): void @@ -24,11 +27,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,25 +42,111 @@ public function testCheckAllProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 15, + 20, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 16, + 21, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 25, + 30, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 26, + 31, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 35, + 43, + ], + ]); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Property object{foo: int, bar?: string}::$foo is not writable.', + 18, + ], + [ + 'Property object{foo: int}|stdClass::$foo is not writable.', + 42, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Property ConflictingAnnotationProperty\PropertyWithAnnotation::$test is not writable.', + 27, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $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, ], ]); } + #[RequiresPhp('>= 8.4')] + public function testBug12553(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/../Variables/data/bug-12553.php'], []); + } + + public function testPrivatePropertyTagRead(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/private-property-tag-read.php'], [ + [ + 'Property PrivatePropertyTagRead\Foo::$foo is not writable.', + 22, + ], + ]); + } + + public function testBug11241(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-11241.php'], [ + [ + 'Property Bug11241\MagicProp::$id is not writable.', + 27, + ], + [ + 'Property Bug11241\ActualProp::$id is not writable.', + 30, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13530(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-13530.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/bug-7074.neon b/tests/PHPStan/Rules/Properties/bug-7074.neon new file mode 100644 index 0000000000..328df07063 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/bug-7074.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-7074.stub diff --git a/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php new file mode 100644 index 0000000000..316a4e136e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php @@ -0,0 +1,20 @@ +getName() === 'TestInitializedProperty\\TestAdditionalConstructor') { + return ['setTwo']; + } + + return []; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php new file mode 100644 index 0000000000..230de9d816 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractFinalHookParseError; + +abstract class User +{ + final abstract public string $bar { + get; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php new file mode 100644 index 0000000000..baba303bf1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php @@ -0,0 +1,15 @@ += 8.4 + +namespace AbstractFinalHook; + +abstract class User +{ + abstract public string $foo { + final get; + } +} + +abstract class Foo +{ + abstract public int $i { final get { return 1;} set; } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php new file mode 100644 index 0000000000..d035d36810 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } + + public abstract string $lastName { + get => $this->lastName; + set => $this->lastName = $value; + } + + public abstract string $middleName { + get => $this->name; + set; + } + + public abstract string $familyName { + get; + set; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..b34e66a886 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractPrivateHook; + +abstract class Foo +{ + abstract private int $i { get; } + abstract protected int $ii { get; } + abstract public int $iii { get; } +} diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php index 434d25be5d..1189bc2232 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php @@ -33,22 +33,22 @@ public function doFoo($foo) } while (is_null($foo) && $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->fooProperty) { - + break; } while (!is_null($foo) && $foo->fooProperty) { - + break; } while (!is_null($foo) || $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->barProperty) { - + break; } while (!is_null($foo) && $foo->barProperty) { - + break; } } diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php 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 cade6d5480..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 { @@ -418,3 +423,20 @@ function mustNotReport(?\stdClass $nullable): bool } } + +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 @@ + + */ + public array $innerTypeExpressions = []; + + /** + * @param \Closure(self): void $callback + */ + public function walkTypes(\Closure $callback): void + { + $startIndexOffset = 0; + + foreach ($this->innerTypeExpressions as $k => ['start_index' => $startIndexOrig, + 'expression' => $inner,]) { + $this->innerTypeExpressions[$k]['start_index'] += $startIndexOffset; + + $innerLengthOrig = \strlen($inner->value); + + $inner->walkTypes($callback); + + $this->value = substr_replace( + $this->value, + $inner->value, + $startIndexOrig + $startIndexOffset, + $innerLengthOrig + ); + + $startIndexOffset += \strlen($inner->value) - $innerLengthOrig; + } + + $callback($this); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-11241.php b/tests/PHPStan/Rules/Properties/data/bug-11241.php new file mode 100644 index 0000000000..37b2782adf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11241.php @@ -0,0 +1,31 @@ +id; } + /** @param mixed $v */ + public function __set(string $name, $v): void {} +} + +/** @property-read int $id */ +class ActualProp +{ + private int $id = 0; + + /** @return mixed */ + public function __get(string $name) { echo $this->id; } + /** @param mixed $v */ + public function __set(string $name, $v): void {} +} + +function (): void { + $x = new MagicProp; + $x->id = 1; + + $x = new ActualProp; + $x->id = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-11241b.php b/tests/PHPStan/Rules/Properties/data/bug-11241b.php new file mode 100644 index 0000000000..21cb4263bd --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11241b.php @@ -0,0 +1,14 @@ +property1 = 'foo'; diff --git a/tests/PHPStan/Rules/Properties/data/bug-11275.php b/tests/PHPStan/Rules/Properties/data/bug-11275.php new file mode 100644 index 0000000000..42b1333560 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11275.php @@ -0,0 +1,52 @@ + + */ + 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-11289.php b/tests/PHPStan/Rules/Properties/data/bug-11289.php new file mode 100644 index 0000000000..89ac44c02a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11289.php @@ -0,0 +1,37 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11289; + +abstract class SomeAbstractClass +{ + private bool $someValue = true; + + /** + * @phpstan-assert-if-true =static $other + */ + public function equals(?self $other): bool + { + return $other instanceof static + && $this->someValue === $other->someValue; + } +} + +class SomeConcreteClass extends SomeAbstractClass +{ + public function __construct( + private bool $someOtherValue, + ) {} + + public function equals(?SomeAbstractClass $other): bool + { + return parent::equals($other) + && $this->someOtherValue === $other->someOtherValue; + } +} + +$a = new SomeConcreteClass(true); +$b = new SomeConcreteClass(false); + +var_dump($a->equals($b), $b->equals($b)); diff --git a/tests/PHPStan/Rules/Properties/data/bug-11424.php b/tests/PHPStan/Rules/Properties/data/bug-11424.php new file mode 100644 index 0000000000..bef983be29 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11424.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug11424; + +/** + * @param object{hello?: string} $a + */ +function hello(object $a): void +{ + echo $a->hello; + echo $a->hello ?? 'hello'; + if (isset($a->hello)) { + echo 'hi'; + } +} + +class Foo +{ + + public int $i; + +} + +class Bar +{ + +} + +function hello2(Foo|Bar $a): void +{ + echo $a->i; + echo $a->i ?? 'hello'; + if (isset($a->i)) { + echo 'hi'; + } +} 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-11761-bis.php b/tests/PHPStan/Rules/Properties/data/bug-11761-bis.php new file mode 100644 index 0000000000..f1714403f5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11761-bis.php @@ -0,0 +1,8 @@ + */ + private array $pipes; + + public function sayHello(string $cmd): void { + proc_open($cmd, [0 => ['pipe', 'r']], $this->pipes); + } +} 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..891b946af3 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 { @@ -37,6 +40,12 @@ public function __construct() } +function (Baz $baz): void { + $baz->foo = 'foo'; // OK + $baz->bar = 'bar'; // OK + $baz->untypedBar = 123; // error +}; + trait DecoratorTrait { 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-12645.php b/tests/PHPStan/Rules/Properties/data/bug-12645.php new file mode 100644 index 0000000000..d4d08d5c10 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12645.php @@ -0,0 +1,25 @@ +id; + } +} + +function (): void { + $foo = new Foo(); + + var_dump($foo->id ?? 2); + var_dump($foo->id); +}; + +function (Foo $foo): void { + var_dump($foo->id ?? 2); + var_dump($foo->id); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-12675.php b/tests/PHPStan/Rules/Properties/data/bug-12675.php new file mode 100644 index 0000000000..ea4674dd3f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12675.php @@ -0,0 +1,37 @@ +username = array_shift($pieces); + $this->domain = array_shift($pieces); + + echo "{$this->username}@{$this->domain}"; + } + + public function with_pop(string $email): void + { + $pieces = explode("@", $email); + if (2 !== count($pieces)) { + + throw new \Exception("Bad, very bad..."); + } + + $this->domain = array_pop($pieces); + $this->username = array_pop($pieces); + + echo "{$this->username}@{$this->domain}"; + } +} 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-12775.php b/tests/PHPStan/Rules/Properties/data/bug-12775.php new file mode 100644 index 0000000000..bdade366f3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12775.php @@ -0,0 +1,17 @@ + + */ + public array $list = []; + + public function bug(int $offset): void + { + if (isset($this->list[$offset])) { + $this->list[$offset] = 123; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13093c.php b/tests/PHPStan/Rules/Properties/data/bug-13093c.php new file mode 100644 index 0000000000..3fecc5f91d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13093c.php @@ -0,0 +1,49 @@ + + */ + private array $nextMutantProcessKillerContainer = []; + + private string $prop; + + public function fillBucketOnce(array &$killer): int + { + if ($this->nextMutantProcessKillerContainer !== []) { + $this->prop = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + +} + +final class ParallelProcessRunner2 +{ + /** + * @var array + */ + private array $nextMutantProcessKillerContainer = []; + + private string $prop; + + public function fillBucketOnce(array &$killer): int + { + $name = 'prop'; + if ($this->nextMutantProcessKillerContainer !== []) { + $this->{$name} = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-13093d.php b/tests/PHPStan/Rules/Properties/data/bug-13093d.php new file mode 100644 index 0000000000..1682c7c22a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13093d.php @@ -0,0 +1,50 @@ + + */ + private array $nextMutantProcessKillerContainer = []; + + static private string $prop; + + public function fillBucketOnce(array &$killer): int + { + if ($this->nextMutantProcessKillerContainer !== []) { + self::$prop = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + +} + +final class ParallelProcessRunner2 +{ + /** + * @var array + */ + private array $nextMutantProcessKillerContainer = []; + + static private string $prop; + + public function fillBucketOnce(array &$killer): int + { + $name = 'prop'; + if ($this->nextMutantProcessKillerContainer !== []) { + self::${$name} = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + +} + + diff --git a/tests/PHPStan/Rules/Properties/data/bug-13123.php b/tests/PHPStan/Rules/Properties/data/bug-13123.php new file mode 100644 index 0000000000..bee3556169 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13123.php @@ -0,0 +1,28 @@ +currentGroup = $this->currentGroup; + + return $control; + } + + protected function createContainer2(Container $control): Container + { + $control->currentGroup = $this->currentGroup; + + return $control; + } + + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13271.php b/tests/PHPStan/Rules/Properties/data/bug-13271.php new file mode 100644 index 0000000000..03f6d8a321 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13271.php @@ -0,0 +1,15 @@ + 0.5 ? 'example_one' : 'example_two'; + $result = $object->$field; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438.php b/tests/PHPStan/Rules/Properties/data/bug-13438.php new file mode 100644 index 0000000000..4abf3e09e9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438; + +use LogicException; + +class Test +{ + /** + * @param non-empty-list $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_shift($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_shift($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438b.php b/tests/PHPStan/Rules/Properties/data/bug-13438b.php new file mode 100644 index 0000000000..78378d2fe5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438b.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438b; + +use LogicException; + +class Test +{ + /** + * @param non-empty-list $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_pop($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_pop($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438c.php b/tests/PHPStan/Rules/Properties/data/bug-13438c.php new file mode 100644 index 0000000000..d12ed84ef1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438c.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug13438c; + +use LogicException; + +class Test +{ + /** + * @var list + */ + private $queue; + + /** + * @param non-empty-list $queue + */ + public function __construct( + array $queue, + ) + { + $this->queue = $queue; + } + + public function test1(): int + { + return array_shift($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_shift($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438d.php b/tests/PHPStan/Rules/Properties/data/bug-13438d.php new file mode 100644 index 0000000000..5e25e051eb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438d.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug13438d; + +class Test +{ + /** + * @param array{} $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_push($this->queue, 1); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438e.php b/tests/PHPStan/Rules/Properties/data/bug-13438e.php new file mode 100644 index 0000000000..ec885da21f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438e.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug13438e; + +class Test +{ + /** + * @param array{} $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_unshift($this->queue, 1); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438f.php b/tests/PHPStan/Rules/Properties/data/bug-13438f.php new file mode 100644 index 0000000000..e053ec3275 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438f.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438f; + +use function PHPStan\dumpType; +use function PHPStan\Testing\assertType; + +class Test +{ + /** + * @param array> $queue + */ + public function __construct( + private array $queue, + ) { + } + + public function test1(): void + { + array_shift($this->queue[5]); // no longer is non-empty-list after this + } + + public function test2(): void + { + $this->queue[5] = []; // normally it works thanks to processAssignVar + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13530.php b/tests/PHPStan/Rules/Properties/data/bug-13530.php new file mode 100644 index 0000000000..b6b9d19f0f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13530.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug13530; + +/** @property-read string $url */ +class HelloWorld +{ + + private string $url; + + public function __construct(string $url) { + $this->url = $url; + } + + public function __get(string $key): mixed { + if ($key === 'url') { + return $this->url; + } + + return NULL; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13537.php b/tests/PHPStan/Rules/Properties/data/bug-13537.php new file mode 100644 index 0000000000..11f51ec1a9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13537.php @@ -0,0 +1,64 @@ + + */ + protected array $test = []; +} + +/** + * @property stdClass $test + */ +class Bar extends Foo +{ + +} + +function (): void { + $bar = new Bar(); + $bob = $bar->test->bob; +}; + +class BaseModel +{ + /** + * @var array + */ + protected array $attributes = []; + + public function __get(string $key): mixed { + return $this->attributes[$key] ?? null; + } + + public function __isset(string $key): bool { + return isset($this->attributes[$key]); + } +} + +/** + * @property stdClass $attributes + * @property bool $other + */ +class Bar2 extends BaseModel +{ + public function __constructor(): void { + $this->attributes = [ + 'attributes' => (object) array('foo' => 'bar'), + 'other' => true + ]; + } +} + +function (): void { + $bar = new Bar2(); + echo $bar->attributes->foo; + + assertType(stdClass::class, $bar->attributes); +}; 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-2888.php b/tests/PHPStan/Rules/Properties/data/bug-2888.php new file mode 100644 index 0000000000..27de191b2b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-2888.php @@ -0,0 +1,28 @@ +prop, 'string'); + array_unshift($this->prop, 'string'); + } + + /** + * @return void + */ + public function bar() + { + $this->prop[] = 'string'; + } +} 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-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-393.php b/tests/PHPStan/Rules/Properties/data/bug-393.php new file mode 100644 index 0000000000..530f7054b8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-393.php @@ -0,0 +1,34 @@ +privateProperty = 123; + }, + null, + Foo::class + ))(); + + (\Closure::bind( + static function () { + $bar = new Bar(); + $bar->privateProperty = 123; + }, + null, + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4174.php b/tests/PHPStan/Rules/Properties/data/bug-4174.php new file mode 100644 index 0000000000..9e4d4e7b09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4174.php @@ -0,0 +1,95 @@ + + */ + public static function getArrayConstAsKey(): array { + return [ + self::NUMBER_TYPE_OFF => 'Off', + self::NUMBER_TYPE_HEAD => 'Head', + self::NUMBER_TYPE_POSITION => 'Position', + ]; + } + + /** + * @return list + */ + public static function getArrayConstAsValue(): array { + return [ + self::NUMBER_TYPE_OFF, + self::NUMBER_TYPE_HEAD, + self::NUMBER_TYPE_POSITION, + ]; + } + + public function checkConstViaArrayKey(): void + { + $numberArray = self::getArrayConstAsKey(); + + // --- + + $newvalue = $this->getIntFromPost('newValue'); + + if ($newvalue && array_key_exists($newvalue, $numberArray)) { + $this->newValue = $newvalue; + } + + if (isset($numberArray[$newvalue])) { + $this->newValue = $newvalue; + } + + // --- + + $newvalue = $this->getIntFromPostWithoutNull('newValue'); + + if ($newvalue && array_key_exists($newvalue, $numberArray)) { + $this->newValue = $newvalue; + } + + if (isset($numberArray[$newvalue])) { + $this->newValue = $newvalue; + } + } + + public function checkConstViaArrayValue(): void + { + $numberArray = self::getArrayConstAsValue(); + + // --- + + $newvalue = $this->getIntFromPost('newValue'); + + if ($newvalue && in_array($newvalue, $numberArray, true)) { + $this->newValue = $newvalue; + } + + // --- + + $newvalue = $this->getIntFromPostWithoutNull('newValue'); + + if ($newvalue && in_array($newvalue, $numberArray, true)) { + $this->newValue = $newvalue; + } + } + + public function getIntFromPost(string $key): ?int { + return isset($_POST[$key]) ? (int)$_POST[$key] : null; + } + + public function getIntFromPostWithoutNull(string $key): int { + return isset($_POST[$key]) ? (int)$_POST[$key] : 0; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4492.php b/tests/PHPStan/Rules/Properties/data/bug-4492.php new file mode 100644 index 0000000000..e253137077 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4492.php @@ -0,0 +1,51 @@ +prop = $prop; + } + + public function getProp(): string + { + return $this->prop; + } +} + +trait PropMangler +{ + /** @var string */ + protected $prop; + + public function mangleProp(): void + { + $this->prop = 'Improved ' . $this->prop; + } +} + +class B extends A +{ + use PropMangler; +} + +class C extends A +{ + /** @var B b */ + public $b; + + public function __construct() + { + $this->b = new B; + } + + public function accessesBProp(): void + { + $this->b->prop = "This works"; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4526.php b/tests/PHPStan/Rules/Properties/data/bug-4526.php new file mode 100644 index 0000000000..86604b4c65 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4526.php @@ -0,0 +1,19 @@ +|null + */ + private $map; + + public function __construct(){ + $this->map = new SplObjectStorage; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4559.php b/tests/PHPStan/Rules/Properties/data/bug-4559.php new file mode 100644 index 0000000000..e3c0b6952f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4559.php @@ -0,0 +1,14 @@ +error->code)) { + echo $response->error->message ?? ''; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4680.php b/tests/PHPStan/Rules/Properties/data/bug-4680.php new file mode 100644 index 0000000000..b353ded26e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4680.php @@ -0,0 +1,18 @@ +|null */ + private $collection1; + + /** @var \SplObjectStorage */ + private $collection2; + + public function __construct() + { + $this->collection1 = new \SplObjectStorage(); + $this->collection2 = new \SplObjectStorage(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-5336.php b/tests/PHPStan/Rules/Properties/data/bug-5336.php new file mode 100644 index 0000000000..7255342b42 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5336.php @@ -0,0 +1,50 @@ +query = $query; + } +} + +abstract class Test +{ + /** + * @var Pager + */ + private $pager; + + /** + * @template T of object + * @param class-string $originalClassName + * @return T&Stub + */ + abstract public function createStub(string $originalClassName): Stub; + + public function sayHello(): void + { + $query = $this->createStub(ProxyQueryInterface::class); + $this->pager = new Pager($query); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5382.php b/tests/PHPStan/Rules/Properties/data/bug-5382.php new file mode 100644 index 0000000000..7ec41886da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5382.php @@ -0,0 +1,73 @@ + + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + $this->providers = $providers ?? [ + new \stdClass(), + ]; + } +} + +class HelloWorld2 +{ + /** + * @var non-empty-list<\stdClass> + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + $this->providers = $providers ?? [ + new \stdClass(), + ]; + + $this->providers = $providers ?: [ + new \stdClass(), + ]; + + $this->providers = $providers !== null ? $providers : [ + new \stdClass(), + ]; + + $providers ??= [ + new \stdClass(), + ]; + $this->providers = $providers; + } +} + +class HelloWorld3 +{ + /** + * @var non-empty-list<\stdClass> + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + /** @var non-empty-list<\stdClass> $newList */ + $newList = [new \stdClass()]; + $newList2 = [new \stdClass()]; + + $this->providers = $providers ?? $newList; + $this->providers = $providers ?? $newList2; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-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 @@ + */ + private Map $map; + + public function __construct() + { + $this->map = new Map(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7839.php b/tests/PHPStan/Rules/Properties/data/bug-7839.php new file mode 100644 index 0000000000..a8d8106762 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7839.php @@ -0,0 +1,26 @@ +table); + assertType('Bug7839\\A|string', $b->table); + assertType('Bug7839\\A|string', $c->table); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-7844.php b/tests/PHPStan/Rules/Properties/data/bug-7844.php new file mode 100644 index 0000000000..b202edd3f2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7844.php @@ -0,0 +1,20 @@ + */ + public $data = array(); + + public function foo(): void + { + if (count($this->data) > 0) { + $this->val = array_shift($this->data); + } + } +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-7844b.php b/tests/PHPStan/Rules/Properties/data/bug-7844b.php new file mode 100644 index 0000000000..c423628f8c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7844b.php @@ -0,0 +1,39 @@ + $objs */ + public function __construct(array $objs) + { + \assert($objs !== []); + $this->p1 = $objs[0]; + + \assert($objs !== []); + $this->p2 = $objs[array_key_last($objs)]; + + \assert($objs !== []); + $this->p3 = \array_pop($objs); + + \assert($objs !== []); + $this->p4 = \array_shift($objs); + + \assert($objs !== []); + $p = \array_shift($objs); + $this->p5 = $p; + + \assert($objs !== []); + $this->doSomething(\array_pop($objs)); + } + + private function doSomething(Obj $obj): void {} +} 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-8282.php b/tests/PHPStan/Rules/Properties/data/bug-8282.php new file mode 100644 index 0000000000..faaa9a103a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8282.php @@ -0,0 +1,31 @@ += 8.0 + +namespace Bug8282; + +/** + * @phpstan-type record array{id: positive-int, name: string} + */ +class Collection +{ + /** @param list $list */ + public function __construct( + public array $list + ) + { + } + + public function updateName(int $index, string $name): void + { + assert(isset($this->list[$index])); + $this->list[$index]['name'] = $name; + } + + public function updateNameById(int $id, string $name): void + { + foreach ($this->list as $index => $entry) { + if ($entry['id'] === $id) { + $this->list[$index]['name'] = $name; + } + } + } +} 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-8668-bis.php b/tests/PHPStan/Rules/Properties/data/bug-8668-bis.php new file mode 100644 index 0000000000..fd561672ba --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8668-bis.php @@ -0,0 +1,25 @@ +sample; // ok + } +} + +class Sample2 { + private $sample = 'abc'; + + public function test(): void { + echo self::$sample; + echo isset(self::$sample); + + echo $this->sample; // ok + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8668.php b/tests/PHPStan/Rules/Properties/data/bug-8668.php new file mode 100644 index 0000000000..3a8c74eadc --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8668.php @@ -0,0 +1,25 @@ +sample; + echo isset($this->sample); + + echo self::$sample; // ok + } +} + +class Sample2 { + private static $sample = 'abc'; + + public function test(): void { + echo $this->sample; + echo isset($this->sample); + + echo self::$sample; // ok + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8825.php b/tests/PHPStan/Rules/Properties/data/bug-8825.php new file mode 100644 index 0000000000..e1aa5c372d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8825.php @@ -0,0 +1,24 @@ +isBool = $actionParameters['my_key'] ?? false; + } + + public function use(): void + { + $this->isBool->someMethod(); + } +} 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-9706.php b/tests/PHPStan/Rules/Properties/data/bug-9706.php new file mode 100644 index 0000000000..55e87a8992 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9706.php @@ -0,0 +1,15 @@ +attributes; + // According to the php.net docs, $length should be a public read-only property. + // See https://www.php.net/manual/en/class.domnamednodemap.php + $length = $attributes->length; + } +} 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/discussion-13274.php b/tests/PHPStan/Rules/Properties/data/discussion-13274.php new file mode 100644 index 0000000000..9c701bb6aa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/discussion-13274.php @@ -0,0 +1,12 @@ +{$foo} = 'bar'; 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/dynamic-stringable-access.php b/tests/PHPStan/Rules/Properties/data/dynamic-stringable-access.php new file mode 100644 index 0000000000..ed31e5ed12 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/dynamic-stringable-access.php @@ -0,0 +1,42 @@ +{$this}->name; + echo $this->var->$this; + echo $this->$this->$name; + echo $this->$this->name; + echo $this->$object; + echo $this->$array; + + echo $this->$name; // valid + echo $this->$stringable; // valid + echo $this->{1111}; // valid + echo $this->{true}; // valid + echo $this->{false}; // valid + echo $this->{null}; // valid + } + + public function testPropertyAssignments(string $name, Stringable $stringable, object $object): void + { + $this->{$this} = $name; + $this->var->{$this} = $name; + $this->$object = $name; + + $this->$name = $name; // valid + $this->$stringable = $name; // valid + $this->{1111} = $name; // valid + $this->{true} = $name; // valid + $this->{false} = $name; // valid + $this->{null} = $name; // valid + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/dynamic-stringable-nullsafe-access.php b/tests/PHPStan/Rules/Properties/data/dynamic-stringable-nullsafe-access.php new file mode 100644 index 0000000000..3977592c31 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/dynamic-stringable-nullsafe-access.php @@ -0,0 +1,27 @@ += 8.0 + +namespace DynamicStringableNullsafeAccess; + +use Stringable; + +final class Foo +{ + private self $var; + + public function testNullsafePropertyFetch(string $name, Stringable $stringable, object $object): void + { + echo $this?->{$this}?->name; + echo $this?->var?->$this; + echo $this?->$this?->$name; + echo $this?->$this?->name; + echo $this?->$object; + + echo $this?->$name; // valid + echo $this?->$stringable; // valid + echo $this?->{1111}; // valid + echo $this?->{true}; // valid + echo $this?->{false}; // valid + echo $this?->{null}; // valid + } + +} 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 79cba0ab31..374397cd0a 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -42,6 +42,9 @@ class PrefixedTags /** @psalm-var int */ private $fooPsalm; + /** @phan-var int */ + private $fooPhan; + } /** @@ -93,3 +96,41 @@ class CallableSignature private $cb; } + +class NestedArrayInProperty +{ + + /** + * @var list|null + */ + public $args; + +} + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + /** @var \MissingPropertyTypehint\GenericClassWithDefault */ + private $foo; + + /** @var \MissingPropertyTypehint\GenericClassWithSomeDefaults */ + private $bar; + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php new file mode 100644 index 0000000000..3c5a6bfccf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php @@ -0,0 +1,15 @@ += 8.3 + +namespace MissingReadonlyAnonymousClassPropertyAssign; + +class Foo +{ + + public function doFoo(): void + { + $c = new readonly class () { + public int $foo; + }; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php new file mode 100644 index 0000000000..aa15ff16f0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php @@ -0,0 +1,51 @@ += 8.1 + +namespace MissingReadOnlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + /** @readonly */ + private readonly int $unassigned; + + /** @readonly */ + private readonly int $unassigned2; + + /** @readonly */ + private readonly int $readBeforeAssigned; + + /** @readonly */ + private readonly int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function __construct() + { + $this->assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php new file mode 100644 index 0000000000..82d55e48fa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php @@ -0,0 +1,260 @@ +assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class BarDoubleAssignInSetter +{ + + /** @readonly */ + private int $foo; + + public function setFoo(int $i) + { + // reported in ReadOnlyPropertyAssignRule + $this->foo = $i; + $this->foo = $i; + } + +} + +class TestCase +{ + + /** @readonly */ + private int $foo; + + protected function setUp(): void + { + $this->foo = 1; + } + +} + +class AssignOp +{ + + /** @readonly */ + private int $foo; + + /** @readonly */ + private ?int $bar; + + public function __construct(int $foo) + { + $this->foo .= $foo; + + $this->bar = $this->bar ?? 3; + } + + +} + +class AssignRef +{ + + /** @readonly */ + private int $foo; + + public function __construct(int $foo) + { + $this->foo = &$foo; + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + private int $assigned; + + private int $unassigned; + + private int $unassigned2; + + private int $readBeforeAssigned; + + private int $doubleAssigned; + + public function __construct() + { + $this->assigned = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +trait FooTrait +{ + + /** @readonly */ + private int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + /** @readonly */ + private int $unassigned; + + /** @readonly */ + private int $unassigned2; + + /** @readonly */ + private int $readBeforeAssigned; + + /** @readonly */ + private int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class FooTraitClass +{ + + use FooTrait; + + public function __construct() + { + $this->assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + +} + + +class Entity +{ + + /** @readonly */ + private int $id; // does not complain about being uninitialized because of a ReadWritePropertiesExtension + +} + +trait BarTrait +{ + + /** @readonly */ + public int $foo; + + public function __construct() + { + $this->foo = 17; + } + +} + +class BarClass +{ + + use BarTrait; + +} + +/** @immutable */ +class A +{ + + public string $a; + +} + +class B extends A +{ + + public string $b; + + public function __construct() + { + $b = $this->b; + } + +} + +class C extends B +{ + + public string $c; + + public function __construct() + { + $this->c = ''; + $this->c = ''; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php 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.php b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php index 9c6bbb66c4..d2a693fe64 100644 --- a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php +++ b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php @@ -22,4 +22,11 @@ public function doBar(string $string, ?string $nullableString): void echo $nullableString?->bar ?? 4; } + public function doNull(): void + { + $null = null; + $null->foo; + $null?->foo; + } + } diff --git a/tests/PHPStan/Rules/Properties/data/overriding-final-property.php b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php new file mode 100644 index 0000000000..b41b531c98 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php @@ -0,0 +1,39 @@ += 8.1 + $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..cef079e642 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php @@ -0,0 +1,137 @@ += 8.2 + +namespace Php82DynamicProperties; + +/** + * @template T of object + * + * @property array $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; + } +}; + +readonly class ReadonlyWithMagic +{ + public function __set(string $name, mixed $value): void + { + var_dump('here'); + } + + public function __get(string $name): mixed + { + return 1; + } +} + +function (): void { + $class = new ReadonlyWithMagic(); + if(isset($class->foo)) + { + echo $class->foo; + } +}; 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/private-property-tag-read.php b/tests/PHPStan/Rules/Properties/data/private-property-tag-read.php new file mode 100644 index 0000000000..0493fba6d6 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/private-property-tag-read.php @@ -0,0 +1,24 @@ +foo = 1; + $foo->bar = 2; +}; diff --git a/tests/PHPStan/Rules/Properties/data/private-property-tag-write.php b/tests/PHPStan/Rules/Properties/data/private-property-tag-write.php new file mode 100644 index 0000000000..7248147887 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/private-property-tag-write.php @@ -0,0 +1,24 @@ +foo; + echo $foo->bar; +}; diff --git a/tests/PHPStan/Rules/Properties/data/private-property-with-allowed-property-tag-is-public.php b/tests/PHPStan/Rules/Properties/data/private-property-with-allowed-property-tag-is-public.php new file mode 100644 index 0000000000..a312fbfb70 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/private-property-with-allowed-property-tag-is-public.php @@ -0,0 +1,20 @@ +foo; +}; diff --git a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php index 47288cdfed..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([]); } @@ -313,3 +313,111 @@ public function doFoo() } } + +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-in-interface.php b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php new file mode 100644 index 0000000000..4d104487fb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php @@ -0,0 +1,14 @@ += 7.4 +foo; + echo $o->bar; + echo $o->baz; + + $o->foo = 1; + $o->bar = 2; + $o->baz = 3; + } + + /** + * @param object{foo: int}&\stdClass $o + * @return void + */ + public function doIntersection(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + + /** + * @param object{foo: int}|\stdClass $o + * @return void + */ + public function doUnion(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php b/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php new file mode 100644 index 0000000000..8163bb9f00 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php @@ -0,0 +1,39 @@ += 8.4 + +namespace PropertyAssignRefAsymmetric; + +class Foo +{ + + private(set) int $a; + + protected(set) int $b; + + public(set) int $c; + + public function doFoo() + { + $foo = &$this->a; + $bar = &$this->b; + $bar = &$this->c; + } + +} + +class Bar extends Foo +{ + + public function doBar(Foo $foo) + { + $foo = &$this->a; + $bar = &$this->b; + $bar = &$this->c; + } + +} + +function (Foo $foo): void { + $a = &$foo->a; + $b = &$foo->b; + $c = &$foo->c; +}; diff --git a/tests/PHPStan/Rules/Properties/data/property-assign-ref.php b/tests/PHPStan/Rules/Properties/data/property-assign-ref.php new file mode 100644 index 0000000000..1b49683a01 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-assign-ref.php @@ -0,0 +1,43 @@ += 8.1 + +namespace PropertyAssignRef; + +class Foo +{ + + private readonly int $foo; + + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} + +class Bar +{ + + public function doBar(Foo $foo) + { + $a = &$foo->foo; // private + $b = &$foo->bar; + } + +} + +class Baz +{ + + protected $a; + + private $b; + +} + +function (Baz $b): void { + $z = &$b->a; + $zz = &$b->b; +}; diff --git a/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php new file mode 100644 index 0000000000..1492d7809d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php @@ -0,0 +1,29 @@ +{$column}; + } + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php new file mode 100644 index 0000000000..d0c7d0c88e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php @@ -0,0 +1,14 @@ += 8.5 + +namespace PropertyHookAttributes; + +class Sit +{ + + public int $i { + #[\NoDiscard] + get { + + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php new file mode 100644 index 0000000000..495cc793b0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php @@ -0,0 +1,57 @@ += 8.4 + +namespace PropertyHookAttributes; + +#[\Attribute(\Attribute::TARGET_CLASS)] +class Foo +{ + +} + +#[\Attribute(\Attribute::TARGET_METHOD)] +class Bar +{ + +} + +#[\Attribute(\Attribute::TARGET_ALL)] +class Baz +{ + +} + +class Lorem +{ + + public int $i { + #[Foo] + get { + + } + } + +} + +class Ipsum +{ + + public int $i { + #[Bar] + get { + + } + } + +} + +class Dolor +{ + + public int $i { + #[Baz] + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php new file mode 100644 index 0000000000..58af100248 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php @@ -0,0 +1,20 @@ += 8.4 + +namespace PropertyHooksReadonlyByPhpDocAssign; + +class Foo +{ + + /** @readonly */ + public int $i { + get { + return $this->i + 1; + } + set { + $self = new self(); + $self->i = 1; + + $this->j = 2; + $this->i = $value - 1; + } + } + + /** @readonly */ + public int $j; + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php new file mode 100644 index 0000000000..cd70c3b514 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php @@ -0,0 +1,12 @@ += 8.4 + +namespace Bug12466; + +interface Foo +{ + + public int $a { get; set;} + +} + +class Bar implements Foo +{ + + public string $a; + +} + +interface MoreProps +{ + + public int $a { get; set; } + + public int $b { get; } + + public int $c { set; } + +} + +class TestMoreProps implements MoreProps +{ + + // not writable + public int $a { + get { + return 1; + } + } + + // not readable + public int $b { + set { + $this->a = 1; + } + } + + // not writable + public int $c { + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php new file mode 100644 index 0000000000..5c2cee2758 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php @@ -0,0 +1,40 @@ + */ + private $nonEmpty; + + /** @var list */ + private $listProp; + + /** @var array> */ + private $nestedListProp; + + public function doFoo(int $i, int $j) + { + unset($this->nonEmpty[$i]); + unset($this->listProp[$i]); + unset($this->nestedListProp[$i][$j]); + } + +} + +class Bar +{ + + /** @var array> */ + private $prop; + + /** + * @param int|string $key + */ + public function doFoo($key): void + { + unset($this->prop[$key]['foo']); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php b/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php new file mode 100644 index 0000000000..ee38e072ce --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php @@ -0,0 +1,37 @@ += 8.4 + +namespace ReadAsymmetricVisibility; + +class Foo +{ + + public private(set) int $a; + public protected(set) int $b; + public public(set) int $c; + + public function doFoo(): void + { + echo $this->a; + echo $this->b; + echo $this->c; + } + +} + +class Bar extends Foo +{ + + public function doBar(): void + { + echo $this->a; + echo $this->b; + echo $this->c; + } + +} + +function (Foo $foo): void { + echo $foo->a; + echo $foo->b; + echo $foo->c; +}; diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php new file mode 100644 index 0000000000..435a73706b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php @@ -0,0 +1,30 @@ += 8.0 + +namespace ReadOnlyPropertyPhpDocAllowedPrivateMutation; + +class A +{ + + /** @phpstan-readonly */ + public array $a = []; + +} + +class B +{ + + /** + * @phpstan-readonly + * @phpstan-allow-private-mutation + */ + public array $a = []; + +} + +class C +{ + + /** @phpstan-readonly-allow-private-mutation */ + public array $a = []; + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php new file mode 100644 index 0000000000..3e3c94a909 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ReadOnlyPropertyPhpDoc; + +class Foo +{ + + /** + * @readonly + * @var int + */ + private $foo; + + /** @readonly */ + private $bar; + + /** + * @readonly + * @var int + */ + private $baz = 0; + +} + +final class ErrorResponse +{ + public function __construct( + /** @readonly */ + public string $message = '' + ) + { + } +} + +/** @immutable */ +class A +{ + + public string $a = ''; + +} + +class B extends A +{ + + public string $b = ''; + +} + +class C extends B +{ + + public string $c = ''; + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php new file mode 100644 index 0000000000..1c3a6dae11 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php @@ -0,0 +1,24 @@ += 8.2 + +namespace ReadOnlyPropertyReadonlyClass; + +readonly class Foo +{ + + private int $foo; + private $bar; + private int $baz = 0; + +} + +readonly final class ErrorResponse +{ + public function __construct(public string $message = '') + { + } +} + +readonly class StaticReadonlyProperty +{ + private static int $foo; +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property.php b/tests/PHPStan/Rules/Properties/data/read-only-property.php index 4182f194c4..20cc6d1c0f 100644 --- a/tests/PHPStan/Rules/Properties/data/read-only-property.php +++ b/tests/PHPStan/Rules/Properties/data/read-only-property.php @@ -1,4 +1,4 @@ -= 8.1 += 8.4 + +namespace ReadingWriteOnlyHookedProperties; + +interface Foo +{ + + public int $i { + // virtual, not readable + set; + } + +} + +function (Foo $f): void { + echo $f->i; +}; + +class Bar +{ + + public int $other; + + public int $i { + // virtual, not readable + set { + $this->other = 1; + } + } + +} + +function (Bar $b): void { + echo $b->i; +}; + +class Baz +{ + + public int $i { + // backed, readable + set { + $this->i = 1; + } + } + +} + +function (Baz $b): void { + $b->i = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php index 33051d7ead..2042e0005f 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php @@ -6,5 +6,6 @@ function (?Foo $foo): void { echo $foo?->readOnlyProperty; echo $foo?->usualProperty; + echo $foo?->asymmetricProperty; echo $foo?->writeOnlyProperty; }; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php index 9dd1f23695..cc2b202f82 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php @@ -2,11 +2,16 @@ namespace ReadingWriteOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo { @@ -14,11 +19,13 @@ public function doFoo() { echo $this->readOnlyProperty; echo $this->usualProperty; + echo $this->asymmetricProperty; echo $this->writeOnlyProperty; $self = new self(); echo $self->readOnlyProperty; echo $self->usualProperty; + echo $self->asymmetricProperty; echo $self->writeOnlyProperty; } diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php new file mode 100644 index 0000000000..7de6b1ae03 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php @@ -0,0 +1,27 @@ += 8.1 + +namespace ReadonlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + protected readonly int $bar; + + /** @readonly */ + public readonly int $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php new file mode 100644 index 0000000000..c390bbb6da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -0,0 +1,314 @@ +foo = $foo; // constructor - fine + $this->psalm = $foo; // constructor - fine + $this->phan = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + $this->psalm = $foo; // do not report -allowed private mutation + $this->phan = $foo; // setter - report + } + +} + +class Bar extends Foo +{ + + public function __construct(int $bar) + { + parent::__construct(1); + $this->foo = $foo; // do not report - private property + $this->bar = $bar; // report - not in declaring class + $this->baz = $baz; // report - not in declaring class + $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class + } + + public function setBar(int $bar): void + { + $this->bar = $bar; // report - not in declaring class + $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class + } + +} + +function (Foo $foo): void { + $foo->foo = 1; // do not report - private property + $foo->baz = 2; // report - not in declaring class +}; + +class FooArrays +{ + + /** + * @var array{name:string,age:int} + * @readonly + */ + public $details; + + public function __construct() + { + $this->details = ['name' => 'Foo', 'age' => 25]; + } + + public function doSomething(): void + { + $this->details['name'] = 'Bob'; + $this->details['age'] = 42; + } + +} + +class NotReadonly +{ + + /** @var int */ + private $foo; + + public function setFoo(int $foo): void + { + $this->foo = $foo; // do not report - not readonly + } + +} + +class NotThis +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function __construct(int $foo) + { + $self = new self(1); + $self->foo = $foo; // report - not $this + } + +} + +class PostInc +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function doFoo(): void + { + $this->foo++; + --$this->foo; + + $this->foo += 5; + } + +} + +class ListAssign +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function __construct() + { + [$this->foo] = [1]; + } + + public function setFoo() + { + [$this->foo] = [1]; + } + + public function setBar() + { + list($this->foo) = [1]; + } + +} + +class AssignRefOutsideClass +{ + + public function doFoo(Foo $foo, int $i) + { + $foo->baz = 5; + $foo->baz = &$i; + } + +} + +class Unserialization +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + /** + * @param array $data + */ + public function __unserialize(array $data) : void + { + [$this->foo] = $data; // __unserialize - fine + } + +} + +class TestCase +{ + + /** + * @var int + * @readonly + */ + private $foo; + + protected function setUp(): void + { + $this->foo = 1; // additional constructor - fine + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + /** @var int */ + private $foo; + + protected $bar; + + public $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function __construct() { + $this->a = ''; // constructor - fine + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function __construct() + { + parent::__construct(); + $this->b = ''; // constructor - fine + } + + public function mod() + { + $this->b = 'input'; // setter - report + $this->a = 'input2'; // setter - report + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $this->c = 'input'; // setter - report + } + +} + +class ArrayAccessPropertyFetch +{ + + /** @readonly */ + private \ArrayObject $storage; + + public function __construct() { + $this->storage = new \ArrayObject(); + } + + public function set(\stdClass $class, int $value): void { + $this->storage[$class] = $value; + unset($this->storage[$class]); + $this->storage = new \WeakMap(); // invalid + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php new file mode 100644 index 0000000000..2ce9bb2e08 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php @@ -0,0 +1,20 @@ += 8.1 + +namespace ReadOnlyPropertyAssignRefPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php new file mode 100644 index 0000000000..26bec0835d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php @@ -0,0 +1,96 @@ +foo; + $bar = &$this->bar; + } + +} + +class Bar +{ + + public function doBar(Foo $foo) + { + $a = &$foo->foo; // private + $b = &$foo->bar; + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + /** @var int */ + private $foo; + + /** @var int */ + public $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function mod() + { + $a = &$this->a; + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function mod() + { + $b = &$this->b; + $a = &$this->a; + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $c = &$this->c; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-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-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 index b2b7406014..eea8ff632d 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php @@ -1,4 +1,4 @@ -= 7.4 +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 23ea31efdc..2de103f0b5 100644 --- a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php @@ -2,11 +2,16 @@ namespace WritingToReadOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo { @@ -28,6 +33,9 @@ public function doFoo() $self->usualProperty = 1; $self->usualProperty .= 1; + $self->asymmetricProperty = "1"; + $self->asymmetricProperty = 1; + $self->writeOnlyProperty = 1; $self->writeOnlyProperty .= 1; @@ -35,4 +43,9 @@ public function doFoo() $self->readOnlyProperty = &$s; } + public function doObjectShape() + { + + } + } diff --git a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon new file mode 100644 index 0000000000..6362843ec8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\Properties\TestInitializedProperty + tags: + - phpstan.additionalConstructorsExtension diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php new file mode 100644 index 0000000000..f0f39b2b5e --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -0,0 +1,212 @@ + + */ +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, + ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 171, + ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 175, + ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 182, + ], + [ + 'Impure exit in pure function PureFunction\bug13288b().', + 200, + ], + [ + 'Impure exit in pure function PureFunction\bug13288c().', + 217, + ], + [ + 'Impure exit in pure function PureFunction\bug13288d().', + 230, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testFirstClassCallable(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13201(): void + { + $this->analyse([__DIR__ . '/data/bug-13201.php'], []); + } + + public function testBug12119(): void + { + $this->analyse([__DIR__ . '/data/bug-12119.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php new file mode 100644 index 0000000000..b5a44ee822 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -0,0 +1,242 @@ + + */ +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, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testPureConstructor(): void + { + $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 static 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], + ]); + } + + public function testBug12382(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12382.php'], [ + [ + 'Method Bug12382\FinalHelloWorld1::dummy() is marked as impure but does not have any side effects.', + 25, + ], + [ + 'Method Bug12382\FinalHelloWorld2::dummy() is marked as impure but does not have any side effects.', + 33, + ], + [ + 'Method Bug12382\FinalHelloWorld3::dummy() is marked as impure but does not have any side effects.', + 42, + ], + [ + 'Method Bug12382\FinalHelloWorld4::dummy() is marked as impure but does not have any side effects.', + 53, + ], + ]); + } + +} 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 @@ +testMethod('random_int'); + $b = testFunction('random_int'); + + return $a . $b; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-12224.php b/tests/PHPStan/Rules/Pure/data/bug-12224.php new file mode 100644 index 0000000000..b93de83178 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-12224.php @@ -0,0 +1,91 @@ +prop++; + return $this; + } +} + +final class FinalHelloWorld1 +{ + /** @phpstan-impure */ + public function dummy() : self{ + return $this; + } +} + +class FinalHelloWorld2 +{ + /** @phpstan-impure */ + final public function dummy() : self{ + return $this; + } +} + +/** @final */ +class FinalHelloWorld3 +{ + /** @phpstan-impure */ + public function dummy() : self{ + return $this; + } +} + +class FinalHelloWorld4 +{ + /** + * @final + * @phpstan-impure + */ + public function dummy() : self{ + return $this; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-13201.php b/tests/PHPStan/Rules/Pure/data/bug-13201.php new file mode 100644 index 0000000000..09ed46ef53 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-13201.php @@ -0,0 +1,19 @@ += 8.1 + +namespace PHPStan\Rules\Pure\data\Bug13201; + +enum Foo: string +{ + + case Bar = 'bar'; + case Unknown = 'unknown'; + +} + +/** + * @pure + */ +function createWithFallback(string $type): Foo +{ + return Foo::tryFrom($type) ?? Foo::Unknown; +} diff --git a/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php b/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php new file mode 100644 index 0000000000..0d6e46af12 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php @@ -0,0 +1,103 @@ += 8.1 + +namespace FirstClassCallablePureFunction; + +class Foo +{ + + /** + * @phpstan-pure + */ + function pureFunction() + { + + } + + /** + * @phpstan-impure + */ + function impureFunction() + { + echo ''; + } + + function voidFunction(): void + { + echo 'test'; + } + +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +/** + * @phpstan-pure + */ +function testThese(Foo $foo) +{ + $cb = $foo->pureFunction(...); + $cb(); + + $cb = $foo->impureFunction(...); + $cb(); + + $cb = $foo->voidFunction(...); + $cb(); + + $cb = pureFunction(...); + $cb(); + + $cb = impureFunction(...); + $cb(); + + $cb = voidFunction(...); + $cb(); + + callCallbackImmediately($cb); + + $cb = 'FirstClassCallablePureFunction\\pureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\impureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\voidFunction'; + $cb(); + + $cb = [$foo, 'pureFunction']; + $cb(); + + $cb = [$foo, 'impureFunction']; + $cb(); + + $cb = [$foo, 'voidFunction']; + $cb(); +} + +/** + * @phpstan-pure + * @return int + */ +function callCallbackImmediately(callable $cb): int +{ + return $cb(); +} diff --git a/tests/PHPStan/Rules/Pure/data/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..baa1f755cf --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-constructor.php @@ -0,0 +1,51 @@ += 8.0 + +namespace PureConstructor; + +final class Foo +{ + + private string $prop; + + public static $staticProp = 1; + + /** @phpstan-pure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + self::$staticProp++; + } + +} + +final class Bar +{ + + private string $prop; + + /** @phpstan-impure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + } + +} + +final 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..295e0c364d --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -0,0 +1,251 @@ +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() +{ + ?> + + + + + + exit() // ok, as array_push() will not invoke the function + ); + + $exitingClosure = function () { + exit(); + }; + array_push($a, // error because by ref arg + $exitingClosure // ok, as array_push() will not invoke the function + ); + + takesString("exit"); // ok, as the maybe callable type string is not typed with immediately-invoked-callable +} + +/** @phpstan-pure */ +function takesString(string $s) { +} + +/** @phpstan-pure */ +function bug13288b() +{ + $exitingClosure = function () { + exit(); + }; + + takesMixed($exitingClosure); // error because immediately invoked +} + +/** + * @phpstan-pure + * @param-immediately-invoked-callable $m + */ +function takesMixed(mixed $m) { +} + +/** @phpstan-pure */ +function bug13288c() +{ + $exitingClosure = function () { + exit(); + }; + + takesMaybeCallable($exitingClosure); +} + +/** @phpstan-pure */ +function takesMaybeCallable(?callable $c) { // arguments passed to functions are considered "immediately called" by default +} + +/** @phpstan-pure */ +function bug13288d() +{ + $exitingClosure = function () { + exit(); + }; + takesMaybeCallable2($exitingClosure); +} + +/** @phpstan-pure */ +function takesMaybeCallable2(?\Closure $c) { // Closures are considered "immediately called" +} + +/** @phpstan-pure */ +function bug13288e(MyClass $m) +{ + $exitingClosure = function () { + exit(); + }; + $m->takesMaybeCallable($exitingClosure); +} + +class MyClass { + /** @phpstan-pure */ + function takesMaybeCallable(?callable $c) { // arguments passed to methods are considered "later called" by default + } +} + diff --git a/tests/PHPStan/Rules/Pure/data/pure-method.php b/tests/PHPStan/Rules/Pure/data/pure-method.php new file mode 100644 index 0000000000..eca2976cda --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-method.php @@ -0,0 +1,452 @@ +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(); + } + + +} + +final class PureConstructor +{ + + /** + * @phpstan-pure + */ + public function __construct() + { + + } + +} + +final class ImpureConstructor +{ + + /** + * @phpstan-impure + */ + public function __construct() + { + echo ''; + } + +} + +final class PossiblyImpureConstructor +{ + + public function __construct() + { + + } + +} + +final class TestConstructors +{ + + /** + * @phpstan-pure + */ + public function doFoo(string $s) + { + new PureConstructor(); + new ImpureConstructor(); + new PossiblyImpureConstructor(); + new $s(); + } + +} + +final 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; + } + +} + +final class ExtendingClass extends ToBeExtended +{ + + public function pure(): int + { + echo 'test'; + return 1; + } + + public function impure(): int + { + return 1; + } + +} + +final 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; + } + +} + +final class NoMagicMethods +{ + +} + +final class PureMagicMethods +{ + + /** + * @phpstan-pure + */ + public function __toString(): string + { + return 'one'; + } + +} + +final class MaybePureMagicMethods +{ + + public function __toString(): string + { + return 'one'; + } + +} + +final class ImpureMagicMethods +{ + + /** + * @phpstan-impure + */ + public function __toString(): string + { + sleep(1); + return 'one'; + } + +} + +final class TestMagicMethods +{ + + /** + * @phpstan-pure + */ + public function doFoo( + NoMagicMethods $no, + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + (string) $no; + (string) $pure; + (string) $maybe; + (string) $impure; + } + +} + +final class NoConstructor +{ + +} + +final class TestNoConstructor +{ + + /** + * @phpstan-pure + */ + public function doFoo(): int + { + new NoConstructor(); + + return 1; + } + +} + +final class MaybeCallableFromUnion +{ + + /** + * @phpstan-pure + * @param callable|string $p + */ + public function doFoo($p): int + { + $p(); + + return 1; + } + +} + +final class VoidMethods +{ + + private function doFoo(): void + { + + } + + private function doBar(): void + { + \PHPStan\dumpType(1); + } + + private function doBaz(): void + { + // nop + ; + + // nop + ; + + // nop + ; + } + +} + +final class AssertingImpureVoidMethod +{ + + /** + * @param mixed $value + * @phpstan-assert array $value + * @phpstan-impure + */ + public function assertSth($value): void + { + + } + +} + +final 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; + } +} + +final 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; + } +} + +class CallDateTime +{ + + /** + * @phpstan-pure + */ + public function doFoo(\DateTimeInterface $date): string + { + return $date->format('j. n. Y'); + } + + /** + * @phpstan-pure + */ + public function doFoo2(\DateTime $date): string + { + return $date->format('j. n. Y'); + } + + /** + * @phpstan-pure + */ + public function doFoo3(\DateTimeImmutable $date): string + { + return $date->format('j. n. Y'); + } + +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index 3c53137056..00f0db3ce3 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -2,217 +2,206 @@ namespace PHPStan\Rules\Regexp; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Regex\RegexExpressionHelper; +use PHPUnit\Framework\Attributes\DataProvider; +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 static 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..5616a2bfc4 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -0,0 +1,95 @@ + + */ +class RegularExpressionQuotingRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RegularExpressionQuotingRule( + self::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, + ], + ], + ); + } + + #[RequiresPhp('>= 8.0')] + public function testRulePhp8(): void + { + $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..5bbb53b83d --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php @@ -0,0 +1,43 @@ + + */ +class RestrictedClassConstantUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::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..1a72aef601 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php @@ -0,0 +1,42 @@ + + */ +class RestrictedFunctionCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RestrictedFunctionCallableUsageRule( + self::getContainer(), + self::createReflectionProvider(), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..d96412eab9 --- /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(), + self::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..3d4706feb9 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php @@ -0,0 +1,42 @@ + + */ +class RestrictedMethodCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new RestrictedMethodCallableUsageRule( + self::getContainer(), + self::createReflectionProvider(), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $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..dd7aac599a --- /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(), + self::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..2fd50f13ad --- /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(), + self::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..353b46ba09 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -0,0 +1,57 @@ + + */ +class RestrictedStaticMethodCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::createReflectionProvider(); + return new RestrictedStaticMethodCallableUsageRule( + self::getContainer(), + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + ); + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-method-callable.php'], [ + [ + 'Cannot call doFoo', + 36, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12951(): void + { + require_once __DIR__ . '/../InternalTag/data/bug-12951-define.php'; + $this->analyse([__DIR__ . '/../InternalTag/data/bug-12951-static-method.php'], [ + [ + 'Call to static method doBar() of internal class Bug12951Polyfill\NumberFormatter from outside its root namespace Bug12951Polyfill.', + 10, + ], + ]); + } + + 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..7bbffc6d40 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -0,0 +1,56 @@ + + */ +class RestrictedStaticMethodUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12951(): void + { + require_once __DIR__ . '/../InternalTag/data/bug-12951-define.php'; + $this->analyse([__DIR__ . '/../InternalTag/data/bug-12951-static-method.php'], [ + [ + 'Call to static method doBar() of internal class Bug12951Polyfill\NumberFormatter from outside its root namespace Bug12951Polyfill.', + 7, + ], + ]); + } + + 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..985fc5d779 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php @@ -0,0 +1,54 @@ + + */ +class RestrictedStaticPropertyUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = self::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 function testBug12951(): void + { + require_once __DIR__ . '/../InternalTag/data/bug-12951-define.php'; + $this->analyse([__DIR__ . '/../InternalTag/data/bug-12951-static-property.php'], [ + [ + 'Access to static property $prop of internal class Bug12951Polyfill\NumberFormatter from outside its root namespace Bug12951Polyfill.', + 7, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php new file mode 100644 index 0000000000..660acdad20 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php @@ -0,0 +1,40 @@ + + */ +class RestrictedUsageOfDeprecatedStringCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RestrictedUsageOfDeprecatedStringCastRule( + self::getContainer(), + self::createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-to-string.php'], [ + [ + 'Cannot call __toString', + 11, + ], + ]); + } + + 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..fbbd1c63fe --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/MethodExtension.php @@ -0,0 +1,26 @@ +getName() !== 'doFoo' && $methodReflection->getName() !== '__toString') { + return null; + } + + return RestrictedUsage::create(sprintf('Cannot call %s', $methodReflection->getName()), '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/data/restricted-to-string.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php new file mode 100644 index 0000000000..6742e3116f --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php @@ -0,0 +1,29 @@ +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..e44e1e61a5 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php @@ -0,0 +1,38 @@ + + */ +class ScopeFunctionCallStackRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackRule(); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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..b784adf647 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php @@ -0,0 +1,38 @@ + + */ +class ScopeFunctionCallStackWithParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackWithParametersRule(); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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 3e7935d1fe..ecb1456ddc 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -2,25 +2,25 @@ namespace PHPStan\Rules\TooWideTypehints; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new TooWideArrowFunctionReturnTypehintRule(); + return new TooWideArrowFunctionReturnTypehintRule( + new TooWideTypeCheck(new PropertyReflectionFinder(), true, true), + ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/tooWideArrowFunctionReturnType.php'], [ [ 'Anonymous function never returns null so it can be removed from the return type.', diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index 2febeeddd6..0449ab45ed 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -2,18 +2,26 @@ namespace PHPStan\Rules\TooWideTypehints; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new TooWideClosureReturnTypehintRule(); + return new TooWideClosureReturnTypehintRule( + new TooWideTypeCheck(new PropertyReflectionFinder(), true, true), + ); + } + + public function testBug10312e(): void + { + $this->analyse([__DIR__ . '/data/bug-10312e.php'], []); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..ddc91cbc00 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php @@ -0,0 +1,53 @@ + + */ +class TooWideFunctionParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideFunctionParameterOutTypeRule(new TooWideParameterOutTypeCheck( + new TooWideTypeCheck(new PropertyReflectionFinder(), true, true), + )); + } + + 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.', + ], + ]); + } + + public function testNestedTooWideType(): void + { + $this->analyse([__DIR__ . '/data/nested-too-wide-function-parameter-out-type.php'], [ + [ + 'PHPDoc tag @param-out type array of function NestedTooWideFunctionParameterOutType\doFoo() can be narrowed to array.', + 9, + 'Offset 1 (false) does not accept type bool.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 0323dbe958..992e3182e4 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -2,18 +2,24 @@ namespace PHPStan\Rules\TooWideTypehints; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase { + private bool $reportTooWideBool = false; + + private bool $reportNestedTooWideType = false; + protected function getRule(): Rule { - return new TooWideFunctionReturnTypehintRule(); + return new TooWideFunctionReturnTypehintRule(new TooWideTypeCheck(new PropertyReflectionFinder(), $this->reportTooWideBool, $this->reportNestedTooWideType)); } public function testRule(): void @@ -44,6 +50,85 @@ public function testRule(): void 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', 79, ], + [ + 'Function TooWideFunctionReturnType\conditionalType() never returns string so it can be removed from the return type.', + 90, + ], + ]); + } + + public function testBug11980(): void + { + $this->analyse([__DIR__ . '/data/bug-11980-function.php'], [ + [ + 'Function Bug11980Function\process2() never returns void so it can be removed from the return type.', + 34, + ], + ]); + } + + public function testBug10312a(): void + { + $this->analyse([__DIR__ . '/data/bug-10312a.php'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug13384cPhp82(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Function Bug13384c\doFoo() never returns true so the return type can be changed to false.', + 5, + ], + [ + 'Function Bug13384c\doFoo2() never returns false so the return type can be changed to true.', + 9, + ], + [ + 'Function Bug13384c\doFooPhpdoc() never returns false so the return type can be changed to true.', + 93, + ], + [ + 'Function Bug13384c\doFooPhpdoc2() never returns true so the return type can be changed to false.', + 100, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384cPrePhp82(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Function Bug13384c\doFooPhpdoc() never returns false so the return type can be changed to true.', + 93, + ], + [ + 'Function Bug13384c\doFooPhpdoc2() never returns true so the return type can be changed to false.', + 100, + ], + ]); + } + + public function testBug13384cOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384c.php'], []); + } + + public function testNestedTooWideType(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/nested-too-wide-function-return-type.php'], [ + [ + 'Return type array of function NestedTooWideFunctionReturnType\dataProvider() can be narrowed to array.', + 8, + 'Offset 1 (false) does not accept type bool.', + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..d8dc023a57 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php @@ -0,0 +1,147 @@ + + */ +class TooWideMethodParameterOutTypeRuleTest extends RuleTestCase +{ + + private bool $checkProtectedAndPublicMethods = true; + + protected function getRule(): TRule + { + return new TooWideMethodParameterOutTypeRule( + new TooWideParameterOutTypeCheck( + new TooWideTypeCheck(new PropertyReflectionFinder(), true, true), + ), + $this->checkProtectedAndPublicMethods, + ); + } + + 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::finalDoBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 45, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBazProtected() never assigns null to &$p so it can be removed from the @param-out type.', + 53, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBazPrivate() never assigns null to &$p so it can be removed from the @param-out type.', + 61, + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 76, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 84, + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 89, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBool() never assigns false to &$b so the by-ref type can be changed to true.', + 105, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBool2() never assigns false to &$b so the @param-out type can be changed to true.', + 113, + ], + ]); + } + + public function testRuleWithoutProtectedAndPublic(): void + { + $this->checkProtectedAndPublicMethods = false; + $this->analyse([__DIR__ . '/data/too-wide-method-parameter-out.php'], [ + [ + 'Method TooWideMethodParameterOut\Foo::finalDoBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 45, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBazPrivate() never assigns null to &$p so it can be removed from the @param-out type.', + 61, + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 76, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 84, + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 89, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBool() never assigns false to &$b so the by-ref type can be changed to true.', + 105, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\FinalFoo::doBool2() never assigns false to &$b so the @param-out type can be changed to true.', + 113, + ], + ]); + } + + public function testBug10684(): void + { + $this->analyse([__DIR__ . '/data/bug-10684.php'], []); + } + + public function testBug10687(): void + { + $this->analyse([__DIR__ . '/data/bug-10687.php'], []); + } + + public function testBug12080(): void + { + $this->checkProtectedAndPublicMethods = false; + $this->analyse([__DIR__ . '/data/bug-12080.php'], []); + } + + public function testNestedTooWideType(): void + { + $this->analyse([__DIR__ . '/data/nested-too-wide-method-parameter-out-type.php'], [ + [ + 'PHPDoc tag @param-out type array of method NestedTooWideMethodParameterOutType\Foo::doFoo() can be narrowed to array.', + 12, + 'Offset 1 (false) does not accept type bool.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 7409bc6d9f..af8d717ce7 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -2,18 +2,27 @@ namespace PHPStan\Rules\TooWideTypehints; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase { + private bool $checkProtectedAndPublicMethods = true; + + private bool $reportTooWideBool = false; + + private bool $reportNestedTooWideType = false; + protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule(true); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideTypeCheck(new PropertyReflectionFinder(), $this->reportTooWideBool, $this->reportNestedTooWideType)); } public function testPrivate(): void @@ -43,6 +52,10 @@ public function testPrivate(): void 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', 86, ], + [ + 'Method TooWideMethodReturnType\ConditionalTypeClass::conditionalType() never returns string so it can be removed from the return type.', + 119, + ], ]); } @@ -80,10 +93,207 @@ public function testPublicProtectedWithInheritance(): void public function testBug5095(): void { - $this->analyse([__DIR__ . '/data/bug-5095.php'], [ + $this->analyse([__DIR__ . '/data/bug-5095.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug6158(): void + { + $this->analyse([__DIR__ . '/data/bug-6158.php'], []); + } + + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public static 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, + ], + ], + ]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataAlwaysCheckFinal')] + 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, + ], + ]); + } + + public function testBug10312(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-10312.php'], []); + } + + public function testBug10312b(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-10312b.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10312c(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-10312c.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10312d(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-10312d.php'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug13384c(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Method Bug13384c\Bug13384c::doBar() never returns true so the return type can be changed to false.', + 33, + ], + [ + 'Method Bug13384c\Bug13384c::doBar2() never returns false so the return type can be changed to true.', + 37, + ], + [ + 'Method Bug13384c\Bug13384c::doBarPhpdoc() never returns false so the return type can be changed to true.', + 55, + ], + [ + 'Method Bug13384c\Bug13384Static::doBar() never returns true so the return type can be changed to false.', + 62, + ], + [ + 'Method Bug13384c\Bug13384Static::doBar2() never returns false so the return type can be changed to true.', + 66, + ], + [ + 'Method Bug13384c\Bug13384Static::doBarPhpdoc() never returns false so the return type can be changed to true.', + 84, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384cPrePhp82(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Method Bug13384c\Bug13384c::doBarPhpdoc() never returns false so the return type can be changed to true.', + 55, + ], + [ + 'Method Bug13384c\Bug13384Static::doBarPhpdoc() never returns false so the return type can be changed to true.', + 84, + ], + ]); + } + + public function testBug13384cOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384c.php'], []); + } + + public function testNestedTooWideType(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/nested-too-wide-method-return-type.php'], [ + [ + 'Return type array of method NestedTooWideMethodReturnType\Foo::dataProvider() can be narrowed to array.', + 11, + 'Offset 1 (false) does not accept type bool.', + ], [ - 'Method Bug5095\Parser::unaryOperatorFor() never returns \'not\' so it can be removed from the return type.', - 21, + 'Return type array of method NestedTooWideMethodReturnType\Foo::dataProvider2() can be narrowed to array.', + 28, + 'Offset 0 (int) does not accept type int|null.', ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php new file mode 100644 index 0000000000..873ffc1c9e --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -0,0 +1,134 @@ + + */ +class TooWidePropertyTypeRuleTest extends RuleTestCase +{ + + private bool $reportTooWideBool = false; + + private bool $reportNestedTooWideType = false; + + protected function getRule(): Rule + { + return new TooWidePropertyTypeRule( + new DirectReadWritePropertiesExtensionProvider([]), + new TooWideTypeCheck(new PropertyReflectionFinder(), $this->reportTooWideBool, $this->reportNestedTooWideType), + ); + } + + #[RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug13384(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384.php'], [ + [ + 'Static property Bug13384\ShutdownHandlerFalseDefault::$registered (bool) is never assigned true so the property type can be changed to false.', + 9, + ], + [ + 'Static property Bug13384\ShutdownHandlerTrueDefault::$registered (bool) is never assigned false so the property type can be changed to true.', + 34, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384PrePhp82(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384.php'], []); + } + + public function testBug13384Phpdoc(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384-phpdoc.php'], [ + [ + 'Static property Bug13384Phpdoc\ShutdownHandlerPhpdocTypes::$registered (bool) is never assigned true so the property type can be changed to false.', + 12, + ], + ]); + } + + public function testBug13384b(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-13384b.php'], []); + } + + public function testBug13384bOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384b.php'], []); + } + + public function testBugPR4318(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/bug-pr-4318.php'], []); + } + + public function testNestedTooWideType(): void + { + $this->reportTooWideBool = true; + $this->reportNestedTooWideType = true; + $this->analyse([__DIR__ . '/data/nested-too-wide-property-type.php'], [ + [ + 'Type array of property NestedTooWidePropertyType\Foo::$a can be narrowed to array.', + 9, + 'Offset 1 (false) does not accept type bool.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312.php new file mode 100644 index 0000000000..d2c2aed1a1 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312.php @@ -0,0 +1,48 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug10312c; + +enum Foo: int +{ + case BAR = 1; + case BAZ = 2; +} + +interface ReturnsFoo +{ + /** @return value-of */ + public function returnsFooValue(): int; +} + +class ReturnsBar implements ReturnsFoo +{ + #[\Override] + public function returnsFooValue(): int + { + return Foo::BAR->value; + } +} + +class ReturnsBarWithFinalMethod implements ReturnsFoo +{ + #[\Override] + final public function returnsFooValue(): int + { + return Foo::BAR->value; + } +} + +final class ReturnsBaz implements ReturnsFoo +{ + #[\Override] + public function returnsFooValue(): int + { + return Foo::BAZ->value; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312d.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312d.php new file mode 100644 index 0000000000..41f463d27b --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312d.php @@ -0,0 +1,41 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug10312d; + +enum Foo: int +{ + case BAR = 1; + case BAZ = 2; +} + +class FooBar { + public ?Foo $foo = null; +} + +interface ReturnsFoo +{ + /** @return value-of */ + public function returnsFooValue(): int; + + /** @return value-of|null */ + public function returnsFooOrNullValue(): ?int; +} + +final class ReturnsNullsafeBaz implements ReturnsFoo +{ + #[\Override] + public function returnsFooValue(): int + { + $f = new FooBar(); + return $f->foo?->value; + } + + #[\Override] + public function returnsFooOrNullValue(): ?int + { + $f = new FooBar(); + return $f->foo?->value; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312e.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312e.php new file mode 100644 index 0000000000..c7907aae14 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10312e.php @@ -0,0 +1,44 @@ +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-12080.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-12080.php new file mode 100644 index 0000000000..6d6acf06b4 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-12080.php @@ -0,0 +1,19 @@ += 8.2 + +declare(strict_types=1); + +namespace Bug13384b; + +use function register_shutdown_function; + +final class ShutdownHandlerFooBar +{ + private static false $registered = false; + private static string $message = ''; + + public static function setMessage(string $message): void + { + self::register(); + + self::$message = $message; + } + + private static function register(): void + { + if (self::$registered) { + return; + } + + register_shutdown_function(static function (): void + { + print self::$message; + }); + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php new file mode 100644 index 0000000000..0d0ad4efbf --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php @@ -0,0 +1,106 @@ + + */ +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/bug-pr-4318.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-pr-4318.php new file mode 100644 index 0000000000..6c85b2a519 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-pr-4318.php @@ -0,0 +1,22 @@ +isConnected()) { + return; + } + + $driver->connect(''); + $this->isConnected = $driver->isConnected(); + echo $this->isConnected; + } + +} 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 @@ + $a + * @param-out array $a + */ +function doFoo(array &$a): void +{ + $a = [ + [ + 1, + false, + ], + [ + 2, + false, + ], + ]; +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-function-return-type.php b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-function-return-type.php new file mode 100644 index 0000000000..1ec1fa24e3 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-function-return-type.php @@ -0,0 +1,20 @@ + + */ +function dataProvider(): array +{ + return [ + [ + 1, + false, + ], + [ + 2, + false, + ], + ]; +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-parameter-out-type.php b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-parameter-out-type.php new file mode 100644 index 0000000000..c7d44963a1 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-parameter-out-type.php @@ -0,0 +1,26 @@ + $a + * @param-out array $a + */ + public function doFoo(array &$a): void + { + $a = [ + [ + 1, + false, + ], + [ + 2, + false, + ], + ]; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-return-type.php b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-return-type.php new file mode 100644 index 0000000000..ec3c2593e6 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-method-return-type.php @@ -0,0 +1,107 @@ + + */ + public function dataProvider(): array + { + return [ + [ + 1, + false, + ], + [ + 2, + false, + ], + ]; + } + + /** + * @return array + */ + public function dataProvider2(): array + { + return [ + [ + 1, + ], + [ + 2, + ], + ]; + } + +} + +class ParentClass +{ + + /** + * @return array + */ + public function doFoo(): array + { + return []; + } + +} + +class ChildClass extends ParentClass +{ + + public function doFoo(): array + { + return [ + [1], + [2], + ]; + } + +} + +class ChildClassNull extends ParentClass +{ + + public function doFoo(): array + { + return [ + [null], + [null], + ]; + } + +} + +class ParentClassBool +{ + + /** + * @return array + */ + public function doFoo(): array + { + return []; + } + +} + +class ChildClassTrue extends ParentClassBool +{ + + /** + * @return array + */ + public function doFoo(): array + { + return [ + [true], + ]; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-property-type.php b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-property-type.php new file mode 100644 index 0000000000..f29386cf9e --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/nested-too-wide-property-type.php @@ -0,0 +1,16 @@ + */ + private array $a = []; + + public function doFoo(): void + { + $this->a = [[1, false]]; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/too-wide-function-parameter-out.php b/tests/PHPStan/Rules/TooWideTypehints/data/too-wide-function-parameter-out.php new file mode 100644 index 0000000000..74130b87e2 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/too-wide-function-parameter-out.php @@ -0,0 +1,51 @@ += 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; + } + +} + +class InvalidPhpDoc +{ + + /** @var int|string */ + private int $a; + + 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), self::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, + ], + ]); + } + + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.3')] + public function testNativeTypes(): void + { + $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..d8f5746cd3 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php @@ -0,0 +1,56 @@ + + */ +class ConstantsInTraitsRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new ConstantsInTraitsRule(new PhpVersion($this->phpVersionId)); + } + + public static 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, + [], + ], + ]; + } + + /** + * @param list $errors + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataRule')] + 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..d8b9c54d0c --- /dev/null +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -0,0 +1,95 @@ + + */ +class TraitAttributesRuleTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule + { + $reflectionProvider = self::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, + ), + ); + } + + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.0')] + public function testRule(): void + { + $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, + ], + ]); + } + + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.3')] + public function testBug12011(): void + { + $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, + ], + ]); + } + + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.1')] + public function testBug12281(): void + { + $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, + ], + ]); + } + + #[RequiresPhp('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, + ], + ]); + } + + #[RequiresPhp('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 20dfb5bc4d..030992cc17 100644 --- a/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php +++ b/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php @@ -3,24 +3,21 @@ 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; - protected function getRule(): Rule { - return new CompactVariablesRule($this->checkMaybeUndefinedVariables); + return new CompactVariablesRule(true); } public function testCompactVariables(): void { - $this->checkMaybeUndefinedVariables = true; $this->analyse([__DIR__ . '/data/compact-variables.php'], [ [ 'Call to function compact() contains undefined variable $bar.', diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 7e82582d7d..96b89cf070 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -2,29 +2,30 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DefinedVariableRuleTest extends \PHPStan\Testing\RuleTestCase +class DefinedVariableRuleTest extends RuleTestCase { - /** @var bool */ - private $cliArgumentsVariablesRegistered; + private bool $cliArgumentsVariablesRegistered; - /** @var bool */ - private $checkMaybeUndefinedVariables; + private bool $checkMaybeUndefinedVariables; - /** @var bool */ - private $polluteScopeWithLoopInitialAssignments; + 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, ); } @@ -66,6 +67,10 @@ public function testDefinedVariables(): void 'Undefined variable: $parseStrParameter', 34, ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], [ 'Undefined variable: $foo', 39, @@ -98,12 +103,16 @@ 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, ], [ @@ -207,27 +216,23 @@ public function testDefinedVariables(): void 360, ], [ - 'Undefined variable: $variableInWhileIsset', - 365, - ], - [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 368, ], [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 369, ], [ - 'Undefined variable: $variableInAssign', + 'Variable $variableInAssign might not be defined.', 384, ], [ - 'Undefined variable: $undefinedArrayIndex', + 'Variable $undefinedArrayIndex might not be defined.', 409, ], [ - 'Undefined variable: $anotherUndefinedArrayIndex', + 'Variable $anotherUndefinedArrayIndex might not be defined.', 409, ], [ @@ -309,7 +314,7 @@ public function testCliArgumentsVariablesRegistered(): void ]); } - public function dataLoopInitialAssignments(): array + public static function dataLoopInitialAssignments(): array { return [ [ @@ -345,15 +350,13 @@ public function dataLoopInitialAssignments(): array } /** - * @dataProvider dataLoopInitialAssignments - * @param bool $polluteScopeWithLoopInitialAssignments - * @param bool $checkMaybeUndefinedVariables - * @param mixed[][] $expectedErrors + * @param list $expectedErrors */ + #[DataProvider('dataLoopInitialAssignments')] public function testLoopInitialAssignments( bool $polluteScopeWithLoopInitialAssignments, bool $checkMaybeUndefinedVariables, - array $expectedErrors + array $expectedErrors, ): void { $this->cliArgumentsVariablesRegistered = false; @@ -460,7 +463,7 @@ public function testForeach(): void ]); } - public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array { return [ [ @@ -569,11 +572,9 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array } /** - * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach - * - * @param bool $polluteScopeWithAlwaysIterableForeach - * @param mixed[] $errors + * @param list $errors */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { $this->cliArgumentsVariablesRegistered = true; @@ -603,10 +604,6 @@ public function testBooleanOperatorsTruthyFalsey(): void public function testArrowFunctions(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -625,10 +622,6 @@ public function testArrowFunctions(): void public function testCoalesceAssign(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -757,10 +750,6 @@ public function testClosureUse(): void public function testNullsafeIsset(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -818,4 +807,366 @@ public function testBug3283(): void $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'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug8142(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug9474(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9474.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testEnum(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug10418(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-10418.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testPassByReferenceIntoNotNullable(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/property-hooks.php'], [ + [ + 'Undefined variable: $val', + 16, + ], + [ + 'Undefined variable: $value', + 28, + ], + [ + 'Undefined variable: $val', + 43, + ], + [ + 'Undefined variable: $value', + 51, + ], + ]); + } + + public function testDynamicAccess(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/dynamic-access.php'], [ + [ + 'Undefined variable: $bar', + 15, + ], + [ + 'Undefined variable: $bar', + 18, + ], + [ + 'Undefined variable: $buz', + 18, + ], + [ + 'Variable $foo might not be defined.', + 36, + ], + [ + 'Variable $foo might not be defined.', + 37, + ], + [ + 'Variable $bar might not be defined.', + 38, + ], + [ + 'Variable $bar might not be defined.', + 40, + ], + [ + 'Variable $foo might not be defined.', + 41, + ], + [ + 'Variable $bar might not be defined.', + 42, + ], + [ + 'Undefined variable: $buz', + 44, + ], + [ + 'Undefined variable: $foo', + 45, + ], + [ + 'Undefined variable: $bar', + 46, + ], + [ + 'Undefined variable: $buz', + 49, + ], + [ + 'Variable $bar might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 50, + ], + [ + 'Variable $bar might not be defined.', + 51, + ], + ]); + } + + public function testBug8719(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-8719.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 5a300c8cda..98a52dfae7 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -5,7 +5,10 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -13,16 +16,15 @@ class EmptyRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new EmptyRule(new IssetCheck( new PropertyDescriptor(), new PropertyReflectionFinder(), true, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, )); } @@ -31,32 +33,37 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/empty-rule.php'], [ [ - 'Offset \'nonexistent\' on array(?0 => bool, ?1 => false, 2 => bool, 3 => false, 4 => true) in empty() does not exist.', + 'Offset \'nonexistent\' on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() does not exist.', 22, ], [ - 'Offset 3 on array(?0 => bool, ?1 => false, 2 => bool, 3 => false, 4 => true) in empty() always exists and is always falsy.', + 'Offset 3 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is always falsy.', 24, ], [ - 'Offset 4 on array(?0 => bool, ?1 => false, 2 => bool, 3 => false, 4 => true) in empty() always exists and is not falsy.', + 'Offset 4 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is not falsy.', 25, ], [ - 'Offset 0 on array(\'\', \'0\', \'foo\', \'\'|\'foo\') in empty() always exists and is always falsy.', + '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.', + '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.', + 'Offset 2 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is not falsy.', 38, ], [ @@ -81,4 +88,147 @@ public function testBug970(): void ]); } + public function testBug6974(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6974.php'], [ + [ + 'Variable $a in empty() always exists and is always falsy.', + 12, + ], + ]); + } + + public function testBug6974TreatPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6974.php'], [ + [ + 'Variable $a in empty() always exists and is always falsy.', + 12, + ], + [ + 'Variable $a in empty() always exists and is not falsy.', + 30, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7109(): void + { + $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 static 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 testBug10367(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10367.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testIssetAfterRememberedConstructor(): void + { + $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 8989023c5e..da52dc925f 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 PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,8 +15,7 @@ class IssetRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -23,7 +23,7 @@ protected function getRule(): Rule new PropertyDescriptor(), new PropertyReflectionFinder(), true, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, )); } @@ -32,6 +32,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -45,11 +50,11 @@ public function testRule(): void 41, ], [ - 'Offset \'string\' on array(1, 2, 3) in isset() does not exist.', + '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, ], [ @@ -57,15 +62,15 @@ public function testRule(): void 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.', + '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.', + '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.', + 'Offset \'b\' on array{} in isset() does not exist.', 79, ], [ @@ -109,11 +114,11 @@ public function testRule(): void 124, ], [ - "Offset 'foo' on array('foo' => string) in isset() always exists and is not nullable.", + '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.", + 'Offset \'bar\' on array{bar: 1} in isset() always exists and is not nullable.', 173, ], ]); @@ -132,11 +137,11 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void 41, ], [ - 'Offset \'string\' on array(1, 2, 3) in isset() does not exist.', + '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, ], [ @@ -144,15 +149,15 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void 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.', + '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.', + '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.', + 'Offset \'b\' on array{} in isset() does not exist.', 79, ], [ @@ -200,9 +205,6 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void public function testNativePropertyTypes(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/isset-native-property-types.php'], [ /*[ @@ -228,10 +230,7 @@ public function testBug4290(): void public function testBug4671(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-4671.php'], [[ - 'Offset string&numeric on array in isset() does not exist.', - 13, - ]]); + $this->analyse([__DIR__ . '/data/bug-4671.php'], []); } public function testVariableCertaintyInIsset(): void @@ -271,11 +270,13 @@ public function testVariableCertaintyInIsset(): void 112, ], [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', + // could be Variable $variableInFirstCase in isset() always exists and is not nullable. + 'Variable $variableInFirstCase in isset() is never defined.', 116, ], [ - 'Variable $variableInSecondCase in isset() always exists and is always null.', + // could be Variable $variableInSecondCase in isset() always exists and is not nullable. + 'Variable $variableInSecondCase in isset() is never defined.', 117, ], [ @@ -322,12 +323,185 @@ public function testIssetInGlobalScope(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], [ + [ + 'Using nullsafe property access "?->bla" in isset() is unnecessary. Use -> instead.', + 10, + ], + ]); + } + #[RequiresPhp('>= 8.0')] + public function testBug7109(): void + { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], []); + + $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 18, + ], + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 29, + ], + [ + 'Expression in isset() is not nullable.', + 41, + ], + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 67, + ], + [ + 'Expression in isset() is not nullable.', + 74, + ], + ]); + } + + public function testBug7318(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], [ + [ + "Offset 'unique' on array{unique: bool} in isset() always exists and is not nullable.", + 27, + ], + ]); + } + + public function testBug6163(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-6163.php'], []); + } + + public function testBug6997(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-6997.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7776(): void + { + $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'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testVirtualProperty(): void + { + $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 testBug11708(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-11708.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, + ], + ]); + } + + public function testPr4374(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/pr-4374.php'], [ + [ + 'Offset string on array in isset() always exists and is not nullable.', + 23, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 60778f3b38..7f9df15081 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -5,35 +5,35 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NullCoalesceRuleTest extends \PHPStan\Testing\RuleTestCase +class NullCoalesceRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new NullCoalesceRule(new IssetCheck( new PropertyDescriptor(), new PropertyReflectionFinder(), true, - $this->treatPhpDocTypesAsCertain + $this->shouldTreatPhpDocTypesAsCertain(), )); } - protected function shouldTreatPhpDocTypesAsCertain(): bool + public function shouldNarrowMethodScopeFromConstructor(): bool { - return $this->treatPhpDocTypesAsCertain; + return true; } public function testCoalesceRule(): void { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/null-coalesce.php'], [ + $errors = [ [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', 32, @@ -43,11 +43,11 @@ public function testCoalesceRule(): void 41, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ?? does not exist.', + '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, ], [ @@ -55,15 +55,15 @@ public function testCoalesceRule(): void 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.', + '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 \'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.', + '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.', + 'Offset \'b\' on array{} on left side of ?? does not exist.', 79, ], [ @@ -118,28 +118,26 @@ 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, - ], - [ - 'Variable $foo on left side of ?? is never defined.', - 141, - ], - [ - 'Variable $bar on left side of ?? is never defined.', - 143, - ], - ]); + ]; + } + $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.', @@ -150,11 +148,11 @@ public function testCoalesceAssignRule(): void 41, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ??= does not exist.', + '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, ], [ @@ -162,15 +160,15 @@ public function testCoalesceAssignRule(): void 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.', + '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 \'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.', + '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.', + 'Offset \'b\' on array{} on left side of ??= does not exist.', 79, ], [ @@ -202,17 +200,11 @@ public function testCoalesceAssignRule(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/null-coalesce-nullsafe.php'], []); } 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.', @@ -231,11 +223,6 @@ public function testVariableCertaintyInNullCoalesce(): void public function testVariableCertaintyInNullCoalesceAssign(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ [ 'Variable $scalar on left side of ??= always exists and is not nullable.', @@ -254,7 +241,6 @@ public function testVariableCertaintyInNullCoalesceAssign(): void 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.', @@ -263,4 +249,102 @@ public function testNullCoalesceInGlobalScope(): void ]); } + public function testBug5933(): void + { + $this->analyse([__DIR__ . '/data/bug-5933.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug7109(): void + { + $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->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->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->analyse([__DIR__ . '/data/bug-7968.php'], []); + } + + public function testBug8084(): void + { + $this->analyse([__DIR__ . '/data/bug-8084.php'], []); + } + + public function testBug10577(): void + { + $this->analyse([__DIR__ . '/data/bug-10577.php'], []); + } + + public function testBug11708(): void + { + $this->analyse([__DIR__ . '/data/bug-11708.php'], []); + } + + public function testBug10610(): void + { + $this->analyse([__DIR__ . '/data/bug-10610.php'], []); + } + + public function testBugDoctrine(): void + { + $this->analyse([__DIR__ . '/data/bug-doctrine.php'], []); + } + + #[RequiresPhp('>= 8.4')] + public function testBug12553(): void + { + $this->analyse([__DIR__ . '/data/bug-12553.php'], []); + } + + public function testIssetAfterRememberedConstructor(): void + { + $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..bdd78d2dfd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -0,0 +1,84 @@ + + */ +class ParameterOutAssignedTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ParameterOutAssignedTypeRule( + new RuleLevelHelper(self::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>, list, 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'], []); + } + + public function testBug13093(): void + { + $this->analyse([__DIR__ . '/data/bug-13093.php'], []); + } + + public function testBug13093b(): void + { + $this->analyse([__DIR__ . '/data/bug-13093b.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12754(): void + { + $this->analyse([__DIR__ . '/data/bug-12754.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php new file mode 100644 index 0000000000..c2af7472ea --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -0,0 +1,66 @@ + + */ +class ParameterOutExecutionEndTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParameterOutExecutionEndTypeRule( + new RuleLevelHelper(self::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'], []); + } + + public function testBug12330(): void + { + $this->analyse([__DIR__ . '/data/bug-12330.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php deleted file mode 100644 index f674af7523..0000000000 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ /dev/null @@ -1,53 +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, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - ] - ); - } - - 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 f1903324ce..9f071f96e9 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,15 +2,26 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +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 +44,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, @@ -58,4 +65,162 @@ public function testBug4289(): void $this->analyse([__DIR__ . '/data/bug-4289.php'], []); } + public function testBug4204(): void + { + $this->analyse([__DIR__ . '/data/bug-4204.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); + } + + #[RequiresPhp('>= 8.4')] + public function testUnsetHookedProperty(): void + { + $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.', + 14, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$finalClass because it might have hooks in a subclass.', + 86, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$nonFinalClass because it might have hooks in a subclass.', + 91, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.', + 93, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$foo because it might have hooks in a subclass.', + 94, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 96, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 97, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$user because it might have hooks in a subclass.', + 98, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 100, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 101, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 102, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 103, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$arrayOfUsers because it might have hooks in a subclass.', + 104, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php index 4371c48567..b9e7a170f0 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 PHPUnit\Framework\Attributes\RequiresPhp; /** - * @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(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testClone(): void @@ -35,11 +38,26 @@ 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', ], ]); } + #[RequiresPhp('>= 8.0')] + public function testRuleWithNullsafeVariant(): void + { + $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 @@ + 123, 2 => 413, 4 => 132], + [1 => 123, 2 => 413, 4 => 132], +]; + +$packingSlipPdf = new PackingSlipPdf($testArray, "TestName"); diff --git a/tests/PHPStan/Rules/Variables/data/bug-10418.php b/tests/PHPStan/Rules/Variables/data/bug-10418.php new file mode 100644 index 0000000000..d193c95e69 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10418.php @@ -0,0 +1,12 @@ += 8.1 + +namespace Bug10418; + +function (): void { + $text = '123'; + $result = match(1){ + preg_match('/(\d+)/', $text, $match) => 'matched number: ' . $match[1], + preg_match('/(\w+)/', $text, $match) => 'matched word: ' . json_encode($match), + default => 'no matches!' + }; +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-10577.php b/tests/PHPStan/Rules/Variables/data/bug-10577.php new file mode 100644 index 0000000000..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 @@ +>} $options + * @param-out array{items: list>} $options + */ +function alterItems(array &$options): void +{ + foreach ($options['items'] as $i => $item) { + $options['items'][$i]['options']['title'] = $item['name']; + } +} + +/** + * @param array{items: array>} $options + * @param-out array{items: array>} $options + */ +function alterItems2(array &$options): void +{ + foreach ($options['items'] as $i => $item) { + $options['items'][$i]['options']['title'] = $item['name']; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-12421.php b/tests/PHPStan/Rules/Variables/data/bug-12421.php new file mode 100644 index 0000000000..9ed7c9b217 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12421.php @@ -0,0 +1,110 @@ += 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-12754.php b/tests/PHPStan/Rules/Variables/data/bug-12754.php new file mode 100644 index 0000000000..e8269ff4d0 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12754.php @@ -0,0 +1,26 @@ + $list + * @return void + */ + public function modify(array &$list): void + { + foreach ($list as $int => $array) { + $list[$int][1] = $this->apply($array[1]); + } + } + + /** + * @param string $value + * @return string + */ + public function apply(string $value): mixed + { + return $value; + } +} 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-13093.php b/tests/PHPStan/Rules/Variables/data/bug-13093.php new file mode 100644 index 0000000000..0d780ce1a0 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13093.php @@ -0,0 +1,40 @@ + + */ + private array $nextMutantProcessKillerContainer = []; + + /** + * @param MutantProcessContainer[] $bucket + * @param Generator $input + */ + public function fillBucketOnce(array &$bucket, Generator $input, int $threadCount): int + { + if (count($bucket) >= $threadCount || !$input->valid()) { + if ($this->nextMutantProcessKillerContainer !== []) { + $bucket[] = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + + return 1; + } + +} + diff --git a/tests/PHPStan/Rules/Variables/data/bug-13093b.php b/tests/PHPStan/Rules/Variables/data/bug-13093b.php new file mode 100644 index 0000000000..5e462a1cbe --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13093b.php @@ -0,0 +1,26 @@ + + */ + private array $nextMutantProcessKillerContainer = []; + + public function fillBucketOnce(string &$killer): int + { + if ($this->nextMutantProcessKillerContainer !== []) { + $killer = array_shift($this->nextMutantProcessKillerContainer); + } + + return 0; + } + +} + diff --git a/tests/PHPStan/Rules/Variables/data/bug-3391.php b/tests/PHPStan/Rules/Variables/data/bug-3391.php new file mode 100644 index 0000000000..2d57843622 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3391.php @@ -0,0 +1,32 @@ + 1]; + } + + public function test() + { + $data = $this->getArray(); + + $data['foo'] = 'a'; + $data['bar'] = 'b'; + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + + unset($data['id']); + + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + return $data; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-3601.php b/tests/PHPStan/Rules/Variables/data/bug-3601.php new file mode 100644 index 0000000000..4930ab2f49 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3601.php @@ -0,0 +1,15 @@ + 'everything is fine']; +} + +if (isset($a, $c, $c[$a])) { + echo $c[$a]; +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-393.php b/tests/PHPStan/Rules/Variables/data/bug-393.php new file mode 100644 index 0000000000..492ce244e7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-393.php @@ -0,0 +1,31 @@ +privateProperty = 123; + }, + new Foo(), + Foo::class + ))(); + + (\Closure::bind( + function () { + $this->privateProperty = 123; + }, + new Bar(), + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4173.php b/tests/PHPStan/Rules/Variables/data/bug-4173.php new file mode 100644 index 0000000000..9257376c96 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4173.php @@ -0,0 +1,34 @@ + $blocks + */ + public function sayHello(array $blocks): void + { + foreach ($blocks as $block) { + $settings = $block->getSettings(); + + if (isset($settings['name'])) { + // switch name with code key + $settings['code'] = $settings['name']; + unset($settings['name']); + } + } + } +} 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..49bbbc89bb --- /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{id: null, text: null, answer: null})&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', mixed)", $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-doctrine.php b/tests/PHPStan/Rules/Variables/data/bug-doctrine.php new file mode 100644 index 0000000000..9886e052d7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-doctrine.php @@ -0,0 +1,15 @@ += 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 121ee4779f..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; } @@ -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/first-class-callables.php b/tests/PHPStan/Rules/Variables/data/first-class-callables.php new file mode 100644 index 0000000000..c4695a551b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/first-class-callables.php @@ -0,0 +1,60 @@ += 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/isset-after-remembered-constructor.php b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php new file mode 100644 index 0000000000..2c48e6249d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php @@ -0,0 +1,98 @@ += 8.2 + +namespace IssetOrCoalesceOnNonNullableInitializedProperty; + +class User +{ + private ?string $nullableString; + private string $maybeUninitializedString; + private string $string; + + private $untyped; + + public function __construct() + { + if (rand(0, 1)) { + $this->nullableString = 'hello'; + $this->string = 'world'; + $this->maybeUninitializedString = 'something'; + } else { + $this->nullableString = null; + $this->string = 'world 2'; + $this->untyped = 123; + } + } + + public function doFoo(): void + { + if (isset($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (isset($this->nullableString)) { + echo $this->nullableString; + } + if (isset($this->string)) { + echo $this->string; + } + if (isset($this->untyped)) { + echo $this->untyped; + } + } + + public function doBar(): void + { + echo $this->maybeUninitializedString ?? 'default'; + echo $this->nullableString ?? 'default'; + echo $this->string ?? 'default'; + echo $this->untyped ?? 'default'; + } + + public function doFooBar(): void + { + if (empty($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (empty($this->nullableString)) { + echo $this->nullableString; + } + if (empty($this->string)) { + echo $this->string; + } + if (empty($this->untyped)) { + echo $this->untyped; + } + } +} + +class MoreEmptyCases +{ + private false|string $union; + private false $false; + private true $true; + private bool $bool; + + public function __construct() + { + if (rand(0, 1)) { + $this->union = 'nope'; + $this->bool = true; + } elseif (rand(10, 20)) { + $this->union = false; + $this->bool = false; + } + $this->false = false; + $this->true = true; + } + + public function doFoo(): void + { + if (empty($this->union)) { + } + if (empty($this->bool)) { + } + if (empty($this->false)) { + } + if (empty($this->true)) { + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php index f493c1fac6..1d93d3f647 100644 --- a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php +++ b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php @@ -1,4 +1,4 @@ -= 7.4 +foo)) { + + } + + if (isset($o->bar)) { + + } + + if (isset($o->baz)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php b/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php new file mode 100644 index 0000000000..ea9488ba44 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php @@ -0,0 +1,23 @@ += 8.4 + +namespace IssetVirtualProperty; + +class Example { + public \DateTimeImmutable $noon { + get => new \DateTimeImmutable('12:00'); + } + + public ?\DateTimeImmutable $nullableNoon { + get => new \DateTimeImmutable('12:00'); + } + + public function doFoo(): void + { + if (isset($this->noon)) { + + } + if (isset($this->nullableNoon)) { + + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/isstring-certainty.php b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php new file mode 100644 index 0000000000..270e978e68 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php @@ -0,0 +1,22 @@ += 7.4 +doFoo($p); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz(&$p): void + { + unset($p[1]); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz2(&$p): void + { + $p[] = 'str'; + } + + /** + * @param list> $p + * @param-out list> $p + */ + function doBaz3(&$p): void + { + unset($p[1][2]); + } + + function doNoParamOut(string &$p): void + { + $p = 1; + } + + function doNoParamOut2(string &$p): void + { + $p = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php new file mode 100644 index 0000000000..6f55e987cc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php @@ -0,0 +1,106 @@ += 8.0 + +namespace PassByReferenceIntoNotNullable; + +class Foo +{ + + public function doFooNoType(&$test) + { + + } + + public function doFooMixedType(mixed &$test) + { + + } + + public function doFooIntType(int &$test) + { + + } + + public function doFooNullableType(?int &$test) + { + + } + + public function test() + { + $this->doFooNoType($one); + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} + +class FooPhpDocs +{ + + /** + * @param mixed $test + */ + public function doFooMixedType(&$test) + { + + } + + /** + * @param int $test + */ + public function doFooIntType(&$test) + { + + } + + /** + * @param int|null $test + */ + public function doFooNullableType(&$test) + { + + } + + public function test() + { + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/pr-4374.php b/tests/PHPStan/Rules/Variables/data/pr-4374.php new file mode 100644 index 0000000000..8a19e13efd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/pr-4374.php @@ -0,0 +1,34 @@ +methods[$cacheKey][$methodName])) { + $method = $this->findClassReflectionWithMethod(); + if ($method === null) { + return false; + } + $this->methods[$cacheKey][$methodName] = $method; + } + + return isset($this->methods[$cacheKey][$methodName]); + } + + private function findClassReflectionWithMethod( + ): ?Foo + { + if (rand(0,1)) { + return new Foo(); + } + return null; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/property-hooks.php b/tests/PHPStan/Rules/Variables/data/property-hooks.php new file mode 100644 index 0000000000..1fc6f744b2 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/property-hooks.php @@ -0,0 +1,54 @@ += 8.4 + +namespace PropertyHooksVariables; + +class Foo +{ + + public int $i { + set { + $this->i = $value + 10; + } + } + + public int $iErr { + set { + $this->iErr = $val + 10; + } + } + + public int $j { + set (int $val) { + $this->j = $val + 10; + } + } + + public int $jErr { + set (int $val) { + $this->jErr = $value + 10; + } + } + +} + + +class FooShort +{ + + public int $i { + set => $value + 10; + } + + public int $iErr { + set => $val + 10; + } + + public int $j { + set (int $val) => $val + 10; + } + + public int $jErr { + set (int $val) => $value + 10; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php new file mode 100644 index 0000000000..97ba5781cd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php @@ -0,0 +1,154 @@ += 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->publicAnnotatedFinalProperty); + 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; + /** @final */ + public string $publicAnnotatedFinalProperty; + + 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->publicAnnotatedFinalProperty); + 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 044a79b5c7..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/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php index c529c265de..47bb5deb95 100644 --- a/tests/PHPStan/Rules/data/datetime-instantiation.php +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -1,20 +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..054094eebb --- /dev/null +++ b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php @@ -0,0 +1,166 @@ +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-super-type-missing-namespace.php', + sprintf( + 'Missing use statement for assertSuperType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-super-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-super-type-wrong-namespace.php', + sprintf( + 'Function PHPStan\Testing\assertSuperType imported with wrong namespace SomeWrong\Namespace\assertSuperType called in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-super-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'), + ), + ]; + yield [ + __DIR__ . '/data/assert-super-type-case-insensitive.php', + sprintf( + 'Missing use statement for assertSuperTYPe() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-super-type-case-insensitive.php'), + ), + ]; + } + + #[DataProvider('dataFileAssertionFailedErrors')] + public function testFileAssertionFailedErrors(string $filePath, string $errorMessage): void + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage($errorMessage); + + self::gatherAssertTypes($filePath); + } + + public function testVariableOrOffsetDescription(): void + { + $filePath = __DIR__ . '/data/assert-certainty-variable-or-offset.php'; + + [$variableAssert, $offsetAssert] = array_values(self::gatherAssertTypes($filePath)); + + $this->assertSame('variable $context', $variableAssert[4]); + $this->assertSame("offset 'email'", $offsetAssert[4]); + } + + public function testSuperType(): void + { + foreach (self::gatherAssertTypes(__DIR__ . '/data/assert-super-type.php') as $data) { + $this->assertFileAsserts(...$data); + } + } + + public static function dataSuperTypeFailed(): array + { + return self::gatherAssertTypes(__DIR__ . '/data/assert-super-type-failed.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataSuperTypeFailed')] + public function testSuperTypeFailed(...$args): void + { + $this->expectException(AssertionFailedError::class); + $this->assertFileAsserts(...$args); + } + + public function testNonexistentClassInAnalysedFile(): void + { + foreach (self::gatherAssertTypes(__DIR__ . '/../../notAutoloaded/nonexistentClasses.php') as $data) { + $this->assertFileAsserts(...$data); + } + } + + public function testNonexistentClassInAnalysedFileWithError(): void + { + try { + foreach (self::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-super-type-case-insensitive.php b/tests/PHPStan/Testing/data/assert-super-type-case-insensitive.php new file mode 100644 index 0000000000..40137f9dad --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-super-type-case-insensitive.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingTypeCaseSensitive; + +function doFoo(string $s) { + assertSuperTYPe('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-super-type-failed.php b/tests/PHPStan/Testing/data/assert-super-type-failed.php new file mode 100644 index 0000000000..6c6f8dd5e6 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-super-type-failed.php @@ -0,0 +1,9 @@ += 8.0 + +namespace MissingAssertTypeNamespace; + +function doFoo(string $s) { + assertSuperType('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-super-type-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-super-type-wrong-namespace.php new file mode 100644 index 0000000000..6a6afcc511 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-super-type-wrong-namespace.php @@ -0,0 +1,10 @@ += 8.0 + +namespace WrongAssertTypeNamespace; + +use function SomeWrong\Namespace\assertSuperType; + +function doFoo(string $s) { + assertSuperType('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-super-type.php b/tests/PHPStan/Testing/data/assert-super-type.php new file mode 100644 index 0000000000..1270192d57 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-super-type.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingTypeCaseSensitive; + +function doFoo1(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..849d0843b6 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-missing-namespace.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingAssertTypeNamespace; + +function doFoo1(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..4f452560a2 --- /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 doFoo1(string $s) { + assertType('string', $s); +} + diff --git a/tests/PHPStan/Tests/AssertionClass.php b/tests/PHPStan/Tests/AssertionClass.php index c529cc62da..29f419fcad 100644 --- a/tests/PHPStan/Tests/AssertionClass.php +++ b/tests/PHPStan/Tests/AssertionClass.php @@ -2,14 +2,16 @@ namespace PHPStan\Tests; +use function is_int; + class AssertionClass { - /** @throws \PHPStan\Tests\AssertionException */ + /** @throws AssertionException */ public function assertString(?string $arg): bool { if ($arg === null) { - throw new \PHPStan\Tests\AssertionException(); + throw new AssertionException(); } return true; } @@ -18,20 +20,19 @@ public function assertString(?string $arg): bool public static function assertInt(?int $arg): bool { if ($arg === null) { - throw new \PHPStan\Tests\AssertionException(); + throw new AssertionException(); } return true; } /** * @param mixed $arg - * @return bool * @throws AssertionException */ public function assertNotInt($arg): bool { if (is_int($arg)) { - throw new \PHPStan\Tests\AssertionException(); + throw new AssertionException(); } return true; diff --git a/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php b/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php index 1e1afae3e2..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,7 +43,7 @@ public function specifyTypes( MethodReflection $methodReflection, MethodCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { 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 8cc1e7b25a..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,7 +43,7 @@ public function specifyTypes( MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { return new SpecifiedTypes(['$bar' => [$node->getArgs()[0]->value, new IntegerType()]]); diff --git a/tests/PHPStan/Tests/AssertionException.php b/tests/PHPStan/Tests/AssertionException.php index af86b5a256..0b8f4cbc7c 100644 --- a/tests/PHPStan/Tests/AssertionException.php +++ b/tests/PHPStan/Tests/AssertionException.php @@ -2,7 +2,9 @@ namespace PHPStan\Tests; -class AssertionException extends \Exception +use Exception; + +class AssertionException extends Exception { } diff --git a/tests/PHPStan/TrinaryLogicTest.php b/tests/PHPStan/TrinaryLogicTest.php index a532a71836..906422f83a 100644 --- a/tests/PHPStan/TrinaryLogicTest.php +++ b/tests/PHPStan/TrinaryLogicTest.php @@ -2,10 +2,13 @@ namespace PHPStan; -class TrinaryLogicTest extends \PHPStan\Testing\PHPStanTestCase +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class TrinaryLogicTest extends PHPStanTestCase { - public function dataAnd(): array + public static function dataAnd(): array { return [ [TrinaryLogic::createNo(), TrinaryLogic::createNo()], @@ -26,22 +29,27 @@ public function dataAnd(): array ]; } - /** - * @dataProvider dataAnd - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $value - * @param TrinaryLogic ...$operands - */ + #[DataProvider('dataAnd')] public function testAnd( TrinaryLogic $expectedResult, TrinaryLogic $value, - TrinaryLogic ...$operands + TrinaryLogic ...$operands, ): void { $this->assertTrue($expectedResult->equals($value->and(...$operands))); } - public function dataOr(): array + #[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 static function dataOr(): array { return [ [TrinaryLogic::createNo(), TrinaryLogic::createNo()], @@ -62,22 +70,27 @@ public function dataOr(): array ]; } - /** - * @dataProvider dataOr - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $value - * @param TrinaryLogic ...$operands - */ + #[DataProvider('dataOr')] public function testOr( TrinaryLogic $expectedResult, TrinaryLogic $value, - TrinaryLogic ...$operands + TrinaryLogic ...$operands, ): void { $this->assertTrue($expectedResult->equals($value->or(...$operands))); } - public function dataNegate(): array + #[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 static function dataNegate(): array { return [ [TrinaryLogic::createNo(), TrinaryLogic::createYes()], @@ -86,17 +99,13 @@ public function dataNegate(): array ]; } - /** - * @dataProvider dataNegate - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $operand - */ + #[DataProvider('dataNegate')] public function testNegate(TrinaryLogic $expectedResult, TrinaryLogic $operand): void { $this->assertTrue($expectedResult->equals($operand->negate())); } - public function dataCompareTo(): array + public static function dataCompareTo(): array { $yes = TrinaryLogic::createYes(); $maybe = TrinaryLogic::createMaybe(); @@ -135,31 +144,21 @@ public function dataCompareTo(): array ]; } - /** - * @dataProvider dataCompareTo - * @param TrinaryLogic $first - * @param TrinaryLogic $second - * @param TrinaryLogic|null $expected - */ + #[DataProvider('dataCompareTo')] 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 - */ + #[DataProvider('dataCompareTo')] 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 787f73de19..057ae63a2f 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,11 +17,13 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class HasMethodTypeTest extends \PHPStan\Testing\PHPStanTestCase +class HasMethodTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -37,7 +43,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createYes(), ], [ @@ -47,7 +53,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('foo'), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createNo(), ], [ @@ -65,11 +71,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 +89,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 +105,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(), @@ -136,23 +137,18 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(HasMethodType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -183,41 +179,31 @@ public function dataIsSubTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], ]; } - /** - * @dataProvider dataIsSubTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 a863a3cd17..087bd96910 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,11 +16,14 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; +use const PHP_VERSION_ID; -class HasPropertyTypeTest extends \PHPStan\Testing\PHPStanTestCase +class HasPropertyTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -32,7 +38,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createYes(), ], [ @@ -42,8 +48,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 +61,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 +79,7 @@ public function dataIsSuperTypeOf(): array [ new HasPropertyType('d'), new UnionType([ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new ObjectType('UnknownClass'), ]), TrinaryLogic::createMaybe(), @@ -102,23 +103,18 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(HasPropertyType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -144,41 +140,31 @@ public function dataIsSubTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createMaybe(), ], ]; } - /** - * @dataProvider dataIsSubTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 559e4da6d9..9b7b2cfcaf 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -3,18 +3,23 @@ namespace PHPStan\Type; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function sprintf; -class ArrayTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ArrayTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -53,35 +58,46 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], [ - new ArrayType(new MixedType(), new MixedType(false, StaticTypeFactory::falsey())), + new ArrayType(new MixedType(), new MixedType(subtractedType: StaticTypeFactory::falsey())), new ConstantArrayType([], []), TrinaryLogic::createYes(), ], [ - new ArrayType(new MixedType(), new MixedType(false, new NullType())), + new ArrayType(new MixedType(), new MixedType(subtractedType: new NullType())), 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ArrayType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataAccepts(): array + public static function dataAccepts(): array { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -92,7 +108,7 @@ public function dataAccepts(): array new ConstantArrayType([], []), new ConstantArrayType( [new ConstantIntegerType(0)], - [new MixedType()] + [new MixedType()], ), new ConstantArrayType([ new ConstantIntegerType(0), @@ -100,7 +116,7 @@ public function dataAccepts(): array ], [ new StringType(), new MixedType(), - ]) + ]), ), TrinaryLogic::createYes(), ], @@ -130,27 +146,22 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - * @param ArrayType $acceptingType - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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())), ); } - public function dataDescribe(): array + public static function dataDescribe(): array { return [ [ @@ -163,39 +174,33 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - * @param ArrayType $type - * @param string $expectedDescription - */ + #[DataProvider('dataDescribe')] public function testDescribe( ArrayType $type, - string $expectedDescription + string $expectedDescription, ): void { $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); } - public function dataInferTemplateTypes(): array + public static 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'], ], @@ -203,7 +208,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -211,7 +216,7 @@ public function dataInferTemplateTypes(): array new StringType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -221,11 +226,11 @@ public function dataInferTemplateTypes(): array new UnionType([ new StringType(), new IntegerType(), - ]) + ]), ), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'int|string'], ], @@ -234,16 +239,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'], ], @@ -251,18 +256,16 @@ public function dataInferTemplateTypes(): array } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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..8749301bc7 --- /dev/null +++ b/tests/PHPStan/Type/BenevolentUnionTypeTest.php @@ -0,0 +1,586 @@ + + */ + public static function dataCanAccessProperties(): 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('dataCanAccessProperties')] + public function testCanAccessProperties(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->canAccessProperties(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> canAccessProperties()', $type->describe(VerbosityLevel::precise())), + ); + } + + /** + * @return Iterator + */ + public static function dataHasInstanceProperty(): 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('dataHasInstanceProperty')] + public function testHasInstanceProperty(BenevolentUnionType $type, string $propertyName, TrinaryLogic $expectedResult): void + { + $actualResult = $type->hasInstanceProperty($propertyName); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasProperty()', $type->describe(VerbosityLevel::precise())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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())), + ); + } + + /** + * @return Iterator + */ + public static 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..e210842df7 --- /dev/null +++ b/tests/PHPStan/Type/BitwiseFlagHelperTest.php @@ -0,0 +1,144 @@ +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(self::createReflectionProvider()); + $actual = $analyser->bitwiseOrContainsConstant($expr, $scope, $constName); + $this->assertTrue($expected->equals($actual), sprintf('Expected Trinary::%s but got Trinary::%s.', $expected->describe(), $actual->describe())); + } + +} diff --git a/tests/PHPStan/Type/BooleanTypeTest.php b/tests/PHPStan/Type/BooleanTypeTest.php index 1d67a1a641..c1d189683c 100644 --- a/tests/PHPStan/Type/BooleanTypeTest.php +++ b/tests/PHPStan/Type/BooleanTypeTest.php @@ -2,14 +2,17 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class BooleanTypeTest extends \PHPStan\Testing\PHPStanTestCase +class BooleanTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -45,23 +48,18 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - * @param BooleanType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] public function testAccepts(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - 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 dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new BooleanType(), @@ -94,23 +92,18 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param BooleanType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -151,19 +144,14 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - * @param BooleanType $type - * @param Type $otherType - * @param bool $expectedResult - */ + #[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())) + 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 9695e032ee..7ea4c0f30e 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -13,11 +16,14 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function sprintf; -class CallableTypeTest extends \PHPStan\Testing\PHPStanTestCase +class CallableTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -45,26 +51,29 @@ 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(CallableType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -99,17 +108,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(), ], [ @@ -135,59 +139,45 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 + public static 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' => [ @@ -195,13 +185,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param($templateType('T')), ], - new IntegerType() + new IntegerType(), ), ['T' => 'string'], ], @@ -210,13 +200,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), ], - $templateType('T') + $templateType('T'), ), ['T' => 'int'], ], @@ -226,14 +216,14 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), ['B' => 'int', 'A' => 'DateTime'], ], @@ -245,7 +235,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), ]), new CallableType( @@ -253,7 +243,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), ['B' => 'int', 'A' => 'DateTime'], ], @@ -264,7 +254,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), [], ], @@ -272,22 +262,20 @@ public function dataInferTemplateTypes(): array } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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()), ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -348,30 +336,88 @@ public function dataAccepts(): array new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new GenericClassStringType(new ObjectType(\Closure::class)), + new GenericClassStringType(new ObjectType(Closure::class)), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), ], + [ + 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(isPure: TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(isPure: TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(isPure: TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(isPure: TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], ]; } - /** - * @dataProvider dataAccepts - * @param \PHPStan\Type\CallableType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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 12d3106ca9..bfb87e2fa8 100644 --- a/tests/PHPStan/Type/ClassStringTypeTest.php +++ b/tests/PHPStan/Type/ClassStringTypeTest.php @@ -2,20 +2,24 @@ namespace PHPStan\Type; +use Exception; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; +use function sprintf; class ClassStringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ new ClassStringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], [ @@ -25,7 +29,7 @@ public function dataIsSuperTypeOf(): array ], [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ], [ @@ -36,20 +40,18 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ClassStringType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ClassStringType(), @@ -71,7 +73,7 @@ public function dataAccepts(): iterable yield [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ]; @@ -83,13 +85,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(), ]; @@ -100,23 +102,18 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - * @param \PHPStan\Type\ClassStringType $type - * @param Type $otherType - * @param \PHPStan\TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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 + public static function dataEquals(): array { return [ [ @@ -132,19 +129,14 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - * @param ClassStringType $type - * @param Type $otherType - * @param bool $expectedResult - */ + #[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())) + 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..64ebdddb2f --- /dev/null +++ b/tests/PHPStan/Type/ClosureTypeFactoryTest.php @@ -0,0 +1,85 @@ + 5, 'int']; + yield [ + static fn (): self => new self('name'), + self::class, + ]; + + if (PHP_VERSION_ID < 80000) { + return; + } + + yield [ + static fn (): static => new static('name'), + 'static(' . self::class . ')', + ]; + } + + /** + * @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 static 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 7f4298dd7d..f5ef07239e 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -2,17 +2,21 @@ namespace PHPStan\Type; +use Closure; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class ClosureTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ClosureTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ new ClosureType([], new MixedType(), false), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createMaybe(), ], [ @@ -20,6 +24,11 @@ public function dataIsSuperTypeOf(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new ClosureType([], new MixedType(), false, impurePoints: []), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], [ new ClosureType([], new UnionType([new IntegerType(), new StringType()]), false), new ClosureType([], new IntegerType(), false), @@ -31,7 +40,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], @@ -71,35 +80,30 @@ 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(), ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] 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..8052c4f8ae --- /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 f15c2eaf79..623c86e3ec 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -2,7 +2,11 @@ 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; @@ -10,18 +14,25 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function sprintf; -class ConstantArrayTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ConstantArrayTypeTest extends PHPStanTestCase { - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ConstantArrayType([], []), @@ -126,7 +137,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('name'), @@ -163,7 +174,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('surname'), @@ -180,7 +191,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -216,7 +227,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [1]), + ], optionalKeys: [1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -232,7 +243,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0, [0]), + ], optionalKeys: [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -246,7 +257,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0), + ], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -262,7 +273,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -280,7 +291,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('color'), ], [ @@ -296,7 +307,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('sound'), ], [ @@ -312,14 +323,14 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ new ConstantStringType('s'), new ConstantStringType('m'), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), TrinaryLogic::createYes(), ]; @@ -330,7 +341,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -340,25 +351,77 @@ 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 - */ + #[DataProvider('dataAccepts')] 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())), ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new ConstantArrayType([], []), @@ -459,7 +522,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2), + ], [2]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -471,7 +534,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0]), + ], [2], [0]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -483,11 +546,75 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), TrinaryLogic::createMaybe(), ]; + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ]; + yield [ new ConstantArrayType([], []), new ConstantArrayType([ @@ -496,37 +623,94 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), TrinaryLogic::createMaybe(), ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), + TrinaryLogic::createNo(), + ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param ConstantArrayType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantArrayType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataInferTemplateTypes(): array + public static 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' => [ @@ -538,7 +722,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -548,7 +732,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -561,7 +745,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -571,7 +755,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -582,7 +766,7 @@ public function dataInferTemplateTypes(): array ], [ new StringType(), - ] + ], ), new ConstantArrayType( [ @@ -592,7 +776,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), [], ], @@ -604,7 +788,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), [], ], @@ -616,7 +800,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), ['T' => 'string'], ], @@ -624,35 +808,31 @@ public function dataInferTemplateTypes(): array } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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()), ); } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(ConstantArrayType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsCallable(): iterable + public static function dataIsCallable(): iterable { yield 'zero items' => [ new ConstantArrayType([], []), @@ -673,7 +853,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), @@ -684,7 +864,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('foobar'), ]), TrinaryLogic::createNo(), @@ -706,7 +886,7 @@ public function dataIsCallable(): iterable new ConstantStringType('a'), new ConstantStringType('b'), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createNo(), @@ -717,11 +897,151 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new GenericClassStringType(new ObjectType(\Closure::class)), + new GenericClassStringType(new ObjectType(Closure::class)), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), ]; } + public static 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], isList: TrinaryLogic::createNo()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [2], isList: 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 1cc80e95a5..83260d706c 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; -class ConstantFloatTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ConstantFloatTypeTest extends PHPStanTestCase { - public function dataDescribe(): array + public static function dataDescribe(): array { return [ [ @@ -22,6 +24,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', @@ -29,14 +39,10 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - * @param ConstantFloatType $type - * @param string $expectedDescription - */ + #[DataProvider('dataDescribe')] 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 23fc7a5d63..6e4fa1ead7 100644 --- a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php @@ -2,15 +2,18 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class ConstantIntegerTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ConstantIntegerTypeTest extends PHPStanTestCase { - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ConstantIntegerType(1), @@ -31,23 +34,18 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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())), ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new ConstantIntegerType(1), @@ -68,19 +66,14 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantIntegerType $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())) + 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 a3ecf2825a..d07d72d48a 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -2,155 +2,169 @@ namespace PHPStan\Type\Constant; +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 PHPUnit\Framework\Attributes\DataProvider; +use stdClass; +use Throwable; +use function sprintf; class ConstantStringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\Throwable::class))), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), TrinaryLogic::createMaybe(), ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantStringType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function testGeneralize(): void { - $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&uppercase-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-empty-string', (new ConstantStringType(\stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('class-string', (new ConstantStringType(\stdClass::class, true))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('class-string', (new ConstantStringType(stdClass::class, true))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('class-string', (new ConstantStringType('NonexistentClass', true))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); } @@ -164,4 +178,11 @@ public function testShortTextInvalidEncoding(): void $this->assertSame("'\xc3Lorem ipsum dolor'", (new ConstantStringType("\xc3Lorem ipsum dolor"))->describe(VerbosityLevel::value())); } + public function testSetInvalidValue(): void + { + $string = new ConstantStringType('internal:/node/add'); + $result = $string->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertInstanceOf(ErrorType::class, $result); + } + } diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php new file mode 100644 index 0000000000..185127d984 --- /dev/null +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -0,0 +1,90 @@ +&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..a4e8527c93 --- /dev/null +++ b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php @@ -0,0 +1,219 @@ += 8.1')] + #[DataProvider('dataIsSuperTypeOf')] + public function testIsSuperTypeOf(Type $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 static 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(), + ]; + } + + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataAccepts')] + public function testAccepts( + Type $type, + Type $acceptedType, + TrinaryLogic $expectedResult, + ): void + { + $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 82ec2ff7eb..d0f5efc7b5 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\PHPStanTestCase +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); + self::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,10 +175,10 @@ public function testFileWithCyclicPhpDocs(): void \CyclicPhpDocs\Foo::class, null, 'getIterator', - '/** @return iterable | Foo */' + '/** @return iterable | Foo */', ); - /** @var \PHPStan\PhpDoc\Tag\ReturnTag $returnTag */ + /** @var ReturnTag $returnTag */ $returnTag = $resolved->getReturnTag(); $this->assertSame('CyclicPhpDocs\Foo|iterable', $returnTag->getType()->describe(VerbosityLevel::precise())); } diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index f99c0afefd..878e1d49f4 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -2,15 +2,18 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class FloatTypeTest extends \PHPStan\Testing\PHPStanTestCase +class FloatTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -58,23 +61,19 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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 + public static function dataEquals(): array { return [ [ @@ -120,19 +119,14 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - * @param FloatType $type - * @param Type $otherType - * @param bool $expectedResult - */ + #[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())) + 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 5472a605e9..9f5b836898 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -2,10 +2,16 @@ 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; @@ -14,63 +20,67 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; +use Throwable; +use function sprintf; -class GenericClassStringTypeTest extends \PHPStan\Testing\PHPStanTestCase +class GenericClassStringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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 => [ @@ -78,113 +88,119 @@ 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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\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(), + ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(GenericClassStringType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataAccepts(): array + public static 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 => [ @@ -192,7 +208,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ConstantStringType('NonexistentClass'), TrinaryLogic::createNo(), @@ -202,11 +218,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(), ], @@ -215,7 +231,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ClassStringType(), TrinaryLogic::createYes(), @@ -225,13 +241,13 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithClass('Boo'), 'U', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], @@ -240,7 +256,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new UnionType([new IntegerType(), new StringType()]), TrinaryLogic::createMaybe(), @@ -250,7 +266,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new BenevolentUnionType([new IntegerType(), new StringType()]), TrinaryLogic::createMaybe(), @@ -258,54 +274,50 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] 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 + public static function dataEquals(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), true, ], [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), false, ], [ - new GenericClassStringType(new StaticType($reflectionProvider->getClass(\Exception::class))), - new GenericClassStringType(new StaticType($reflectionProvider->getClass(\Exception::class))), + 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))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(stdClass::class))), false, ], ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(GenericClassStringType $type, Type $otherType, bool $expected): void { $verbosityLevel = VerbosityLevel::precise(); @@ -316,14 +328,14 @@ public function testEquals(GenericClassStringType $type, Type $otherType, bool $ $this->assertSame( $expected, $actual, - sprintf('%s -> equals(%s)', $typeDescription, $otherTypeDescription) + sprintf('%s -> equals(%s)', $typeDescription, $otherTypeDescription), ); $actual = $otherType->equals($type); $this->assertSame( $expected, $actual, - sprintf('%s -> equals(%s)', $otherTypeDescription, $typeDescription) + sprintf('%s -> equals(%s)', $otherTypeDescription, $typeDescription), ); } diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index 72d721b310..2f3733c363 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -2,6 +2,11 @@ namespace PHPStan\Type\Generic; +use DateTime; +use DateTimeInterface; +use Exception; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -12,14 +17,22 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use ReflectionClass; +use stdClass; +use Traversable; +use function array_map; +use function sprintf; +use const PHP_VERSION_ID; -class GenericObjectTypeTest extends \PHPStan\Testing\PHPStanTestCase +class GenericObjectTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ 'equal type' => [ @@ -53,7 +66,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(), ], @@ -87,64 +100,181 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(C\Covariant::class, [new ObjectType('DateTimeInterface')]), TrinaryLogic::createMaybe(), ], + 'contravariant with equal types' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createYes(), + ], + 'contravariant with sub type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createMaybe(), + ], + 'contravariant with super type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + TrinaryLogic::createYes(), + ], [ - new ObjectType(\ReflectionClass::class), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new ObjectType(ReflectionClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), - new ObjectType(\ReflectionClass::class), + new ObjectType(ReflectionClass::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ReflectionClass::class, [ + new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), - TrinaryLogic::createYes(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createYes(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), - new GenericObjectType(\ReflectionClass::class, [ + new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - TrinaryLogic::createMaybe(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\Exception::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(Exception::class), ]), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), TrinaryLogic::createNo(), ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + public static function dataTypeProjections(): array + { + $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], variances: [TemplateTypeVariance::createInvariant()]); + $invariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], variances: [TemplateTypeVariance::createInvariant()]); + $invariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], variances: [TemplateTypeVariance::createInvariant()]); + + $covariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], variances: [TemplateTypeVariance::createCovariant()]); + $covariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], variances: [TemplateTypeVariance::createCovariant()]); + $covariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], variances: [TemplateTypeVariance::createCovariant()]); + + $contravariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], variances: [TemplateTypeVariance::createContravariant()]); + $contravariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], variances: [TemplateTypeVariance::createContravariant()]); + $contravariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], variances: [TemplateTypeVariance::createContravariant()]); + + $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], variances: [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 { $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())), ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ 'equal type' => [ @@ -178,61 +308,58 @@ 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(), ], ]; } - /** - * @dataProvider dataAccepts - */ + #[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 + public static 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'), @@ -241,7 +368,7 @@ public function dataInferTemplateTypes(): array ], 'two types' => [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ @@ -253,12 +380,12 @@ public function dataInferTemplateTypes(): array 'union' => [ new UnionType([ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ new IntegerType(), - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ]), new GenericObjectType(A\A2::class, [ @@ -270,7 +397,7 @@ public function dataInferTemplateTypes(): array 'nested' => [ new GenericObjectType(A\A::class, [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), ]), @@ -284,27 +411,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)), ]), [], ], @@ -319,32 +446,28 @@ public function dataInferTemplateTypes(): array } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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 + public static function dataGetReferencedTypeArguments(): array { - $templateType = static function (string $name, ?Type $bound = null): TemplateType { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $bound ?? new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $bound ?? new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'param: Invariant' => [ @@ -355,7 +478,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -367,7 +490,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -381,7 +504,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -397,7 +520,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(), ), ], ], @@ -409,7 +602,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -421,7 +614,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -435,7 +628,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -451,7 +644,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'), + ], variances: [ + TemplateTypeVariance::createCovariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], variances: [ + TemplateTypeVariance::createContravariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), ), ], ], @@ -465,7 +844,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'), + ], variances: [ + TemplateTypeVariance::createCovariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], variances: [ + TemplateTypeVariance::createContravariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), ), ], ], @@ -473,10 +954,9 @@ public function dataGetReferencedTypeArguments(): array } /** - * @dataProvider dataGetReferencedTypeArguments - * * @param array $expectedReferences */ + #[DataProvider('dataGetReferencedTypeArguments')] public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVariance, Type $type, array $expectedReferences): void { $result = []; @@ -484,19 +964,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 91bfcae0e2..c54813969d 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\PHPStanTestCase +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( + $this->assertSame( '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( + $this->assertSame( 'DateTime&T (function a(), parameter)', - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php index 41ca7505c3..ed6907ac61 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php @@ -2,58 +2,61 @@ namespace PHPStan\Type\Generic; +use Exception; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class TemplateTypeMapTest extends TestCase +class TemplateTypeMapTest extends PHPStanTestCase { - public function dataUnionWithLowerBoundTypes(): iterable + public static function dataUnionWithLowerBoundTypes(): iterable { $map = (new TemplateTypeMap([ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ]))->convertToLowerBoundTypes(); yield [ $map, - \Exception::class, + Exception::class, ]; yield [ $map->union(new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ])), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ $map->union((new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ]))->convertToLowerBoundTypes()), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ (new TemplateTypeMap([ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ], [ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ]))->convertToLowerBoundTypes(), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ (new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ], [ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ]))->convertToLowerBoundTypes(), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; } - /** @dataProvider dataUnionWithLowerBoundTypes */ + #[DataProvider('dataUnionWithLowerBoundTypes')] public function testUnionWithLowerBoundTypes(TemplateTypeMap $map, string $expectedTDescription): void { $this->assertFalse($map->isEmpty()); diff --git a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php index f63858d142..e65f1a963e 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\IntegerType; @@ -9,12 +10,13 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class TemplateTypeVarianceTest extends TestCase +class TemplateTypeVarianceTest extends PHPStanTestCase { - public function dataIsValidVariance(): iterable + public static function dataIsValidVariance(): iterable { foreach ([TemplateTypeVariance::createInvariant(), TemplateTypeVariance::createCovariant()] as $variance) { yield [ @@ -75,26 +77,25 @@ public function dataIsValidVariance(): iterable } } - /** - * @dataProvider dataIsValidVariance - */ + #[DataProvider('dataIsValidVariance')] public function testIsValidVariance( TemplateTypeVariance $variance, Type $a, Type $b, TrinaryLogic $expected, - TrinaryLogic $expectedInversed + TrinaryLogic $expectedInversed, ): void { + $templateType = TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', null, $variance); $this->assertSame( $expected->describe(), - $variance->isValidVariance($a, $b)->describe(), - sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())) + $variance->isValidVariance($templateType, $a, $b)->result->describe(), + sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())), ); $this->assertSame( $expectedInversed->describe(), - $variance->isValidVariance($b, $a)->describe(), - sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())) + $variance->isValidVariance($templateType, $b, $a)->result->describe(), + sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-c.php b/tests/PHPStan/Type/Generic/data/generic-classes-c.php index cd17cc9261..417797587b 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-c.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-c.php @@ -9,3 +9,7 @@ interface Invariant { /** @template-covariant T */ interface Covariant { } + +/** @template-contravariant T */ +interface Contravariant { +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-d.php b/tests/PHPStan/Type/Generic/data/generic-classes-d.php index b25dbc6bac..69621f8e08 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-d.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-d.php @@ -13,3 +13,9 @@ interface Out { /** @return T */ public function get(); } + +/** @template-contravariant T */ +interface In { + /** @return T */ + public function get(); +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-e.php b/tests/PHPStan/Type/Generic/data/generic-classes-e.php new file mode 100644 index 0000000000..ed60625429 --- /dev/null +++ b/tests/PHPStan/Type/Generic/data/generic-classes-e.php @@ -0,0 +1,10 @@ +accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - 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 dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new IntegerType(), @@ -95,23 +93,18 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param IntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IntegerType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -157,19 +150,14 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - * @param IntegerType $type - * @param Type $otherType - * @param bool $expectedResult - */ + #[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())) + 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 03434722b1..7318ba9c65 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -2,19 +2,36 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use stdClass; use Test\ClassWithToString; +use Traversable; +use function count; +use function sprintf; +use const PHP_VERSION_ID; -class IntersectionTypeTest extends \PHPStan\Testing\PHPStanTestCase +class IntersectionTypeTest extends PHPStanTestCase { - public function dataAccepts(): \Iterator + /** + * @return Iterator + */ + public static function dataAccepts(): Iterator { $intersectionType = new IntersectionType([ new ObjectType('Collection'), @@ -36,7 +53,7 @@ public function dataAccepts(): \Iterator yield [ $intersectionType, new IterableType(new MixedType(), new ObjectType('Item')), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; yield [ @@ -51,34 +68,29 @@ 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 + #[DataProvider('dataAccepts')] + 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())), ); } - public function dataIsCallable(): array + public static function dataIsCallable(): array { return [ [ 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')), ]), @@ -101,22 +113,21 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - * @param IntersectionType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): \Iterator + /** + * @return Iterator + */ + public static function dataIsSuperTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -147,82 +158,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 ObjectType(Traversable::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), - 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 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,69 +185,73 @@ 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IntersectionType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + /** + * @return Iterator + */ + public static function dataIsSubTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -335,16 +294,6 @@ public function dataIsSubTypeOf(): \Iterator TrinaryLogic::createNo(), ]; - $intersectionTypeB = new IntersectionType([ - new IntegerType(), - ]); - - yield [ - $intersectionTypeB, - $intersectionTypeB, - TrinaryLogic::createYes(), - ]; - $intersectionTypeC = new IntersectionType([ new StringType(), new CallableType(), @@ -389,42 +338,395 @@ public function dataIsSubTypeOf(): \Iterator ]; } - /** - * @dataProvider dataIsSubTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 static function dataGetEnumCases(): iterable + { + if (PHP_VERSION_ID < 80100) { + return []; + } + + $reflectionProvider = self::createReflectionProvider(); + $classReflection = $reflectionProvider->getClass(FooEnum::class); + + yield [ + new IntersectionType([ + new ThisType($classReflection), + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ]), + [ + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ], + ]; + } + + /** + * @param list $expectedEnumCases + */ + #[DataProvider('dataGetEnumCases')] + 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 static 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 0ac3b7ab07..557caeb1ad 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,11 +11,14 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_map; +use function sprintf; -class IterableTypeTest extends \PHPStan\Testing\PHPStanTestCase +class IterableTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -55,23 +59,18 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IterableType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -157,69 +156,57 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 + public static 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 +214,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -235,7 +222,7 @@ public function dataInferTemplateTypes(): array new StringType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -243,28 +230,26 @@ public function dataInferTemplateTypes(): array } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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()), ); } - public function dataDescribe(): array + public static function dataDescribe(): array { $templateType = TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ @@ -295,9 +280,7 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - */ + #[DataProvider('dataDescribe')] public function testDescribe(Type $type, string $expect): void { $result = $type->describe(VerbosityLevel::typeOnly()); @@ -305,20 +288,20 @@ public function testDescribe(Type $type, string $expect): void $this->assertSame($expect, $result); } - public function dataAccepts(): array + public static function dataAccepts(): array { /** @var TemplateMixedType $t */ $t = TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ [ new IterableType( new MixedType(), - $t + $t, ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -326,7 +309,7 @@ public function dataAccepts(): array [ new IterableType( new MixedType(), - $t->toArgument() + $t->toArgument(), ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -334,19 +317,14 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - * @param IterableType $iterableType - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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 a0cda10b5f..c204153c3f 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -2,13 +2,24 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class MixedTypeTest extends \PHPStan\Testing\PHPStanTestCase +class MixedTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ 0 => [ @@ -22,48 +33,48 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 2 => [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new IntegerType(), TrinaryLogic::createNo(), ], 3 => [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new ConstantIntegerType(1), TrinaryLogic::createNo(), ], 4 => [ - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), new IntegerType(), TrinaryLogic::createMaybe(), ], 5 => [ - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), new MixedType(), TrinaryLogic::createMaybe(), ], 6 => [ new MixedType(), - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), TrinaryLogic::createYes(), ], 7 => [ - new MixedType(false, new ConstantIntegerType(1)), - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), TrinaryLogic::createYes(), ], 8 => [ - new MixedType(false, new IntegerType()), - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new IntegerType()), + new MixedType(subtractedType: new ConstantIntegerType(1)), TrinaryLogic::createMaybe(), ], 9 => [ - new MixedType(false, new ConstantIntegerType(1)), - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new ConstantIntegerType(1)), + new MixedType(subtractedType: new IntegerType()), TrinaryLogic::createYes(), ], 10 => [ - new MixedType(false, new StringType()), - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new StringType()), + new MixedType(subtractedType: new IntegerType()), TrinaryLogic::createMaybe(), ], 11 => [ @@ -72,53 +83,53 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 12 => [ - new MixedType(false, new ObjectWithoutClassType()), + new MixedType(subtractedType: new ObjectWithoutClassType()), new ObjectWithoutClassType(), TrinaryLogic::createNo(), ], 13 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 14 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createYes(), ], 15 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectWithoutClassType(new ObjectType('InvalidArgumentException')), TrinaryLogic::createMaybe(), ], 16 => [ - new MixedType(false, new ObjectType('InvalidArgumentException')), + new MixedType(subtractedType: new ObjectType('InvalidArgumentException')), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createYes(), ], 17 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('Exception'), TrinaryLogic::createNo(), ], 18 => [ - new MixedType(false, new ObjectType('InvalidArgumentException')), + new MixedType(subtractedType: new ObjectType('InvalidArgumentException')), new ObjectType('Exception'), TrinaryLogic::createMaybe(), ], 19 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('InvalidArgumentException'), TrinaryLogic::createNo(), ], 20 => [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new MixedType(), TrinaryLogic::createMaybe(), ], 21 => [ - new MixedType(false, new ObjectType('Exception')), - new MixedType(false, new ObjectType('stdClass')), + new MixedType(subtractedType: new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('stdClass')), TrinaryLogic::createMaybe(), ], 22 => [ @@ -127,7 +138,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 23 => [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), new NeverType(), TrinaryLogic::createYes(), ], @@ -137,27 +148,1031 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 25 => [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(MixedType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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())), + ); + } + + #[DataProvider('dataEquals')] + public function testEquals(MixedType $mixedType, Type $typeToCompare, bool $expectedResult): void + { + $this->assertSame( + $expectedResult, + $mixedType->equals($typeToCompare), + sprintf('%s -> equals(%s)', $mixedType->describe(VerbosityLevel::precise()), $typeToCompare->describe(VerbosityLevel::precise())), + ); + } + + public static function dataEquals(): array + { + return [ + [ + new MixedType(), + new MixedType(), + true, + ], + [ + new MixedType(true), + new MixedType(), + true, + ], + [ + new MixedType(), + new MixedType(true), + true, + ], + [ + new MixedType(), + new MixedType(true, new IntegerType()), + false, + ], + [ + new MixedType(), + new ErrorType(), + false, + ], + [ + new MixedType(true), + new ErrorType(), + false, + ], + ]; + } + } diff --git a/tests/PHPStan/Type/ObjectTypeTest.php b/tests/PHPStan/Type/ObjectTypeTest.php index fdf99391da..3371c28427 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -2,19 +2,54 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +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\PHPStanTestCase +class ObjectTypeTest extends PHPStanTestCase { - public function dataIsIterable(): array + public static function dataIsIterable(): array { return [ [new ObjectType('ArrayObject'), TrinaryLogic::createYes()], @@ -24,22 +59,45 @@ public function dataIsIterable(): array ]; } - /** - * @dataProvider dataIsIterable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsIterable')] public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsCallable(): array + /** + * @return iterable + */ + public static function dataIsEnum(): iterable + { + if (PHP_VERSION_ID >= 80000) { + yield [new ObjectType('UnitEnum'), TrinaryLogic::createYes()]; + yield [new ObjectType('BackedEnum'), TrinaryLogic::createYes()]; + } + yield [new ObjectType('Unknown'), TrinaryLogic::createMaybe()]; + yield [new ObjectType('Countable'), TrinaryLogic::createMaybe()]; + yield [new ObjectType('Stringable'), TrinaryLogic::createNo()]; + yield [new ObjectType('Throwable'), TrinaryLogic::createNo()]; + yield [new ObjectType('DateTime'), TrinaryLogic::createNo()]; + } + + #[DataProvider('dataIsEnum')] + public function testIsEnum(ObjectType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isEnum(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isEnum()', $type->describe(VerbosityLevel::precise())), + ); + } + + public static function dataIsCallable(): array { return [ [new ObjectType('Closure'), TrinaryLogic::createYes()], @@ -48,24 +106,20 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ 0 => [ @@ -74,164 +128,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($reflectionProvider->getClass(\Traversable::class)), + new ObjectType(ArrayAccess::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new ObjectType(\Countable::class), - new StaticType($reflectionProvider->getClass(\Countable::class)), + new ObjectType(Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new ObjectType(\DateTimeImmutable::class), - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new ObjectType(\Traversable::class), - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new ObjectType(\Traversable::class), - new StaticType($reflectionProvider->getClass(\Iterator::class)), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new ObjectType(\ArrayObject::class), - new StaticType($reflectionProvider->getClass(\Traversable::class)), + new ObjectType(ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new ObjectType(\Iterator::class), - new StaticType($reflectionProvider->getClass(\Traversable::class)), + new ObjectType(Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new ObjectType(\ArrayObject::class), - new StaticType($reflectionProvider->getClass(\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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\ArrayObject::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 23 => [ - new ObjectType(\LogicException::class), - new StaticType($reflectionProvider->getClass(\InvalidArgumentException::class)), + new ObjectType(LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 24 => [ - new ObjectType(\InvalidArgumentException::class), - new StaticType($reflectionProvider->getClass(\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'), @@ -239,17 +293,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'), @@ -268,244 +322,427 @@ 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ObjectType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataAccepts(): array + public static 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 - */ + #[DataProvider('dataAccepts')] 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())), + ); + } + + /** + * @return Iterator + */ + public static function dataHasConstant(): Iterator + { + yield [ + new ObjectType(DateTimeImmutable::class), + 'ATOM', + TrinaryLogic::createYes(), + ]; + yield [ + new ObjectType(DateTimeImmutable::class), + 'CUSTOM', + TrinaryLogic::createMaybe(), + ]; + yield [ + new ObjectType(Closure::class), // is final + 'CUSTOM', + TrinaryLogic::createNo(), + ]; + yield [ + new ObjectType('SomeNonExistingClass'), + 'CUSTOM', + TrinaryLogic::createMaybe(), + ]; + } + + #[DataProvider('dataHasConstant')] + public function testHasConstant(ObjectType $type, string $constantName, TrinaryLogic $expectedResult): void + { + $actualResult = $type->hasConstant($constantName); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasConstant("%s")', $type->describe(VerbosityLevel::precise()), $constantName), ); } 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()); } - public function dataHasOffsetValueType(): array + public static function dataHasOffsetValueType(): array { return [ [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Generator::class), + new ObjectType(Generator::class), new IntegerType(), TrinaryLogic::createNo(), ], [ - new ObjectType(\ArrayAccess::class), + new ObjectType(ArrayAccess::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Countable::class), + new ObjectType(Countable::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new MixedType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new StringType(), TrinaryLogic::createNo(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTimeInterface::class), new MixedType()]), - new ObjectType(\DateTime::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTimeInterface::class), new MixedType()]), + new ObjectType(DateTime::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\DateTimeInterface::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\stdClass::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ], ]; } - /** - * @dataProvider dataHasOffsetValueType - * @param \PHPStan\Type\ObjectType $type - * @param Type $offsetType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataHasOffsetValueType')] 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 static 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'), + ], + ]; + } + + /** + * @param list $expectedEnumCases + */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataGetEnumCases')] + public function testGetEnumCases( + ObjectType $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 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 b1692bc73e..69e33b253a 100644 --- a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php +++ b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php @@ -2,12 +2,16 @@ namespace PHPStan\Type; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; -class ObjectWithoutClassTypeTest extends \PHPStan\Testing\PHPStanTestCase +class ObjectWithoutClassTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -26,13 +30,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,31 +50,26 @@ 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(), ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param ObjectWithoutClassType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ObjectWithoutClassType $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())) + 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..872ba25586 --- /dev/null +++ b/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php @@ -0,0 +1,60 @@ +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..f7be3c75f7 --- /dev/null +++ b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php @@ -0,0 +1,91 @@ +', + ]; + 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 655c40052b..980b5f6004 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -2,17 +2,36 @@ namespace PHPStan\Type; +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 PHPUnit\Framework\Attributes\DataProvider; use StaticTypeTest\Base; use StaticTypeTest\Child; use StaticTypeTest\FinalChild; +use stdClass; +use Traversable; +use function sprintf; -class StaticTypeTest extends \PHPStan\Testing\PHPStanTestCase +class StaticTypeTest extends PHPStanTestCase { - public function dataIsIterable(): array + public static function dataIsIterable(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [new StaticType($reflectionProvider->getClass('ArrayObject')), TrinaryLogic::createYes()], @@ -21,24 +40,20 @@ public function dataIsIterable(): array ]; } - /** - * @dataProvider dataIsIterable - * @param StaticType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsIterable')] public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsCallable(): array + public static function dataIsCallable(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [new StaticType($reflectionProvider->getClass('Closure')), TrinaryLogic::createYes()], @@ -46,183 +61,179 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - * @param StaticType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ 1 => [ - new StaticType($reflectionProvider->getClass(\ArrayAccess::class)), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 2 => [ - new StaticType($reflectionProvider->getClass(\Countable::class)), - new ObjectType(\Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), + new ObjectType(Countable::class), TrinaryLogic::createMaybe(), ], 3 => [ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], 4 => [ - new StaticType($reflectionProvider->getClass(\Traversable::class)), - new ObjectType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(ArrayObject::class), TrinaryLogic::createMaybe(), ], 5 => [ - new StaticType($reflectionProvider->getClass(\Traversable::class)), - new ObjectType(\Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(Iterator::class), TrinaryLogic::createMaybe(), ], 6 => [ - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 7 => [ - new StaticType($reflectionProvider->getClass(\Iterator::class)), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 8 => [ - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createNo(), ], 9 => [ - new StaticType($reflectionProvider->getClass(\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($reflectionProvider->getClass(\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($reflectionProvider->getClass(\LogicException::class)), - new ObjectType(\InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createMaybe(), ], 12 => [ - new StaticType($reflectionProvider->getClass(\InvalidArgumentException::class)), - new ObjectType(\LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new ObjectType(LogicException::class), TrinaryLogic::createMaybe(), ], 13 => [ - new StaticType($reflectionProvider->getClass(\ArrayAccess::class)), - new StaticType($reflectionProvider->getClass(\Traversable::class)), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new StaticType($reflectionProvider->getClass(\Countable::class)), - new StaticType($reflectionProvider->getClass(\Countable::class)), + new StaticType($reflectionProvider->getClass(Countable::class)), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new StaticType($reflectionProvider->getClass(\Traversable::class)), - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new StaticType($reflectionProvider->getClass(\Traversable::class)), - new StaticType($reflectionProvider->getClass(\Iterator::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), - new StaticType($reflectionProvider->getClass(\Traversable::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new StaticType($reflectionProvider->getClass(\Iterator::class)), - new StaticType($reflectionProvider->getClass(\Traversable::class)), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createNo(), ], 21 => [ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), ]), TrinaryLogic::createYes(), ], 22 => [ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new StringType(), ]), TrinaryLogic::createMaybe(), ], 23 => [ - new StaticType($reflectionProvider->getClass(\DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType($reflectionProvider->getClass(\ArrayObject::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 24 => [ - new StaticType($reflectionProvider->getClass(\LogicException::class)), - new StaticType($reflectionProvider->getClass(\InvalidArgumentException::class)), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 25 => [ - new StaticType($reflectionProvider->getClass(\InvalidArgumentException::class)), - new StaticType($reflectionProvider->getClass(\LogicException::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new StaticType($reflectionProvider->getClass(LogicException::class)), TrinaryLogic::createMaybe(), ], 26 => [ - new StaticType($reflectionProvider->getClass(\stdClass::class)), + new StaticType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 27 => [ new ObjectWithoutClassType(), - new StaticType($reflectionProvider->getClass(\stdClass::class)), + new StaticType($reflectionProvider->getClass(stdClass::class)), TrinaryLogic::createYes(), ], 28 => [ - new ThisType($reflectionProvider->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 29 => [ new ObjectWithoutClassType(), - new ThisType($reflectionProvider->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), TrinaryLogic::createYes(), ], [ @@ -245,63 +256,207 @@ public function dataIsSuperTypeOf(): array new ObjectType(FinalChild::class), TrinaryLogic::createYes(), ], + [ + new ThisType( + $reflectionProvider->getClass(\ThisSubtractable\Foo::class), // phpcs:ignore + new UnionType([new ObjectType(\ThisSubtractable\Bar::class), new ObjectType(\ThisSubtractable\Baz::class)]), // phpcs:ignore + ), + new UnionType([ + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Bar::class), // phpcs:ignore + ]), + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Baz::class), // phpcs:ignore + ]), + ]), + TrinaryLogic::createNo(), + ], + [ + new GenericStaticType( + $reflectionProvider->getClass(\MethodSignatureGenericStaticType\Foo::class), // phpcs:ignore + [new StringType()], + null, + [], + ), + new GenericObjectType(\MethodSignatureGenericStaticType\FinalBar::class, [ // phpcs:ignore + new IntegerType(), + ]), + TrinaryLogic::createNo(), + ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(Type $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataEquals(): array + public static function dataEquals(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::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($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(Exception::class)), false, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new StaticType($reflectionProvider->getClass(\InvalidArgumentException::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), false, ], ]; } - /** - * @dataProvider dataEquals - * @param StaticType $type - * @param StaticType $otherType - * @param bool $expected - */ + #[DataProvider('dataEquals')] public function testEquals(StaticType $type, StaticType $otherType, bool $expected): void { $this->assertSame($expected, $type->equals($otherType)); $this->assertSame($expected, $otherType->equals($type)); } + public static function dataAccepts(): iterable + { + $reflectionProvider = self::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 c6fd99b935..a5630c1a30 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use Exception; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasPropertyType; @@ -10,17 +11,20 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; use Test\ClassWithToString; +use function sprintf; class StringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ new StringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], [ @@ -29,7 +33,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], @@ -38,7 +42,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new StringType(), TrinaryLogic::createMaybe(), @@ -49,7 +53,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], @@ -58,18 +62,18 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ClassStringType(), TrinaryLogic::createMaybe(), ], [ - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], @@ -78,28 +82,31 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createMaybe(), ], + [ + new StringAlwaysAcceptingObjectWithToStringType(), + new ObjectType(ClassWithToString::class), + TrinaryLogic::createYes(), + ], ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(StringType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new StringType(), @@ -122,7 +129,7 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ]; @@ -132,7 +139,7 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new StringType(), TrinaryLogic::createYes(), @@ -143,10 +150,10 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), new StringType(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; yield [ @@ -154,35 +161,30 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), TrinaryLogic::createYes(), ]; } - /** - * @dataProvider dataAccepts - * @param \PHPStan\Type\StringType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataAccepts')] 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 + public static function dataEquals(): array { return [ [ @@ -218,19 +220,14 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - * @param StringType $type - * @param Type $otherType - * @param bool $expectedResult - */ + #[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())) + 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 73cbbad976..61c8839f13 100644 --- a/tests/PHPStan/Type/TemplateTypeTest.php +++ b/tests/PHPStan/Type/TemplateTypeTest.php @@ -2,59 +2,69 @@ 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 PHPUnit\Framework\Attributes\DataProvider; +use stdClass; +use Throwable; +use Traversable; +use function array_map; +use function assert; +use function sprintf; -class TemplateTypeTest extends \PHPStan\Testing\PHPStanTestCase +class TemplateTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static 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,48 +73,65 @@ 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(), + ], ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] 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 + public static 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 +168,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 +211,7 @@ public function dataIsSuperTypeOf(): array ], 11 => [ $templateType('T', null), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), // T isSuperTypeTo DateTimeInterface TrinaryLogic::createMaybe(), // DateTimeInterface isSuperTypeTo T ], @@ -196,59 +223,59 @@ public function dataIsSuperTypeOf(): array ], 13 => [ $templateType('T', new ObjectWithoutClassType()), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], 14 => [ - $templateType('T', new ObjectType(\Throwable::class)), - new ObjectType(\Exception::class), + $templateType('T', new ObjectType(Throwable::class)), + new ObjectType(Exception::class), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ + 15 => [ $templateType('T', new MixedType(true)), $templateType('U', new UnionType([new IntegerType(), new StringType()])), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ + 16 => [ $templateType('T', new MixedType(true)), $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ - $templateType('T', new ObjectType(\stdClass::class)), + 17 => [ + $templateType('T', new ObjectType(stdClass::class)), $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createNo(), TrinaryLogic::createNo(), ], - [ + 18 => [ $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 19 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new UnionType([new IntegerType(), new StringType()])), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 20 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), - TrinaryLogic::createMaybe(), - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), ], - [ + 21 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new IntegerType()), - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), TrinaryLogic::createMaybe(), ], - [ + 22 => [ $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType()]), TrinaryLogic::createMaybe(), @@ -257,14 +284,12 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf( Type $type, Type $otherType, TrinaryLogic $expectedIsSuperType, - TrinaryLogic $expectedIsSuperTypeInverse + TrinaryLogic $expectedIsSuperTypeInverse, ): void { assert($type instanceof TemplateType); @@ -273,29 +298,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 + public static 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(), @@ -303,51 +325,49 @@ 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'), [], ], ]; } /** - * @dataProvider dataInferTemplateTypes * @param array $expectedTypes */ + #[DataProvider('dataInferTemplateTypes')] public function testResolveTemplateTypes(Type $received, Type $template, array $expectedTypes): void { $result = $template->inferTemplateTypes($received); $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..acb3b932cf 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -3,14 +3,14 @@ namespace PHPStan\Type; use PHPStan\Fixture\TestDecimal; -use PHPUnit\Framework\TestCase; +use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; -class TestDecimalOperatorTypeSpecifyingExtensionTest extends TestCase +class TestDecimalOperatorTypeSpecifyingExtensionTest extends PHPStanTestCase { - /** - * @dataProvider dataSigilAndSidesProvider - */ + #[DataProvider('dataSigilAndSidesProvider')] public function testSupportsMatchingSigilsAndSides(string $sigil, Type $leftType, Type $rightType): void { $extension = new TestDecimalOperatorTypeSpecifyingExtension(); @@ -20,7 +20,7 @@ public function testSupportsMatchingSigilsAndSides(string $sigil, Type $leftType self::assertTrue($result); } - public function dataSigilAndSidesProvider(): iterable + public static function dataSigilAndSidesProvider(): iterable { yield '+' => [ '+', @@ -45,11 +45,21 @@ 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), + ]; } - /** - * @dataProvider dataNotMatchingSidesProvider - */ + #[DataProvider('dataNotMatchingSidesProvider')] public function testNotSupportsNotMatchingSides(string $sigil, Type $leftType, Type $rightType): void { $extension = new TestDecimalOperatorTypeSpecifyingExtension(); @@ -59,18 +69,18 @@ public function testNotSupportsNotMatchingSides(string $sigil, Type $leftType, T self::assertFalse($result); } - public function dataNotMatchingSidesProvider(): iterable + public static 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 61fb1db976..7cfe7237ab 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,31 +2,71 @@ namespace PHPStan\Type; +use Bug9006\TestInterface; +use CheckTypeFunctionCall\FinalClassWithMethodExists; +use CheckTypeFunctionCall\FinalClassWithPropertyExists; +use Closure; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DynamicProperties\FinalFoo; +use Exception; +use InvalidArgumentException; +use Iterator; +use ObjectShapesAcceptance\ClassWithFooIntProperty; +use PHPStan\Fixture\FinalClass; +use PHPStan\Generics\FunctionsAssertType\C; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; 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 PHPUnit\Framework\Attributes\DataProvider; +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\PHPStanTestCase +class TypeCombinatorTest extends PHPStanTestCase { - public function dataAddNull(): array + public static function dataAddNull(): array { return [ [ @@ -89,15 +129,13 @@ public function dataAddNull(): array } /** - * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ + #[DataProvider('dataAddNull')] public function testAddNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::addNull($type); @@ -106,15 +144,13 @@ public function testAddNull( } /** - * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ + #[DataProvider('dataAddNull')] public function testUnionWithNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::union($type, new NullType()); @@ -122,9 +158,9 @@ public function testUnionWithNull( $this->assertInstanceOf($expectedTypeClass, $result); } - public function dataRemoveNull(): array + public static function dataRemoveNull(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [ @@ -185,7 +221,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -193,7 +229,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -211,15 +247,13 @@ public function dataRemoveNull(): array } /** - * @dataProvider dataRemoveNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ + #[DataProvider('dataRemoveNull')] public function testRemoveNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::removeNull($type); @@ -227,9 +261,9 @@ public function testRemoveNull( $this->assertInstanceOf($expectedTypeClass, $result); } - public function dataUnion(): array + public static function dataUnion(): iterable { - return [ + yield from [ [ [ new StringType(), @@ -548,7 +582,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\RecursionCallable\Foo::class), + new ObjectType(Foo::class), new CallableType(), ], UnionType::class, @@ -660,7 +694,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), ], ObjectType::class, @@ -681,7 +715,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -692,8 +726,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}', ], [ [ @@ -701,7 +735,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -710,8 +744,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}', ], [ [ @@ -719,7 +753,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -732,20 +766,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(), ]), ], @@ -844,20 +878,6 @@ public function dataUnion(): array UnionType::class, "'bar'|'barr'|'baz'|'bazz'|'foo'|'fooo'|'lorem'|'loremm'|'loremmm'", ], - [ - [ - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - ], - IntersectionType::class, - 'array&hasOffset(string)', - ], [ [ new IntersectionType([ @@ -883,7 +903,7 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), @@ -896,13 +916,13 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), ], IntersectionType::class, - 'array(object, \'foo\')&callable(): mixed', + 'array{object, \'foo\'}&callable(): mixed', ], [ [ @@ -934,8 +954,8 @@ public function dataUnion(): array new HasOffsetType(new ConstantStringType('bar')), ]), ], - ArrayType::class, - 'array', + IntersectionType::class, + 'non-empty-array', ], [ [ @@ -950,7 +970,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', ], [ [ @@ -962,16 +996,16 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new IntegerType()), - new MixedType(false, new StringType()), + new MixedType(subtractedType: new IntegerType()), + new MixedType(subtractedType: new StringType()), ], MixedType::class, 'mixed=implicit', ], [ [ - new MixedType(false, new IntegerType()), - new MixedType(false, new UnionType([ + new MixedType(subtractedType: new IntegerType()), + new MixedType(subtractedType: new UnionType([ new IntegerType(), new StringType(), ])), @@ -981,8 +1015,8 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new IntegerType()), - new MixedType(false, new UnionType([ + new MixedType(subtractedType: new IntegerType()), + new MixedType(subtractedType: new UnionType([ new ConstantIntegerType(1), new StringType(), ])), @@ -992,8 +1026,8 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new ConstantIntegerType(2)), - new MixedType(false, new UnionType([ + new MixedType(subtractedType: new ConstantIntegerType(2)), + new MixedType(subtractedType: new UnionType([ new ConstantIntegerType(1), new StringType(), ])), @@ -1003,8 +1037,8 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new IntegerType()), - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new IntegerType()), + new MixedType(subtractedType: new ConstantIntegerType(1)), ], MixedType::class, 'mixed~1=implicit', @@ -1012,14 +1046,14 @@ public function dataUnion(): array [ [ new MixedType(false), - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), ], MixedType::class, 'mixed=implicit', ], [ [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), new UnionType([ new StringType(), new NullType(), @@ -1046,15 +1080,15 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new ObjectWithoutClassType(new ObjectType('A')), ], MixedType::class, - 'mixed=implicit', + 'mixed~int=implicit', ], [ [ - new MixedType(false, new ObjectType('A')), + new MixedType(subtractedType: new ObjectType('A')), new ObjectWithoutClassType(new ObjectType('A')), ], MixedType::class, @@ -1062,7 +1096,7 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), new NullType(), ], MixedType::class, @@ -1070,7 +1104,7 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new IntegerType(), ], MixedType::class, @@ -1078,7 +1112,7 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), new ConstantIntegerType(1), ], MixedType::class, @@ -1086,7 +1120,7 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('Throwable'), ], MixedType::class, @@ -1094,7 +1128,7 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('Exception'), ], MixedType::class, @@ -1102,16 +1136,16 @@ public function dataUnion(): array ], [ [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('InvalidArgumentException'), ], MixedType::class, - 'mixed=implicit', // should be MixedType~Exception+InvalidArgumentException + 'mixed~(Exception~InvalidArgumentException)=implicit', ], [ [ new NullType(), - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), ], MixedType::class, 'mixed=implicit', @@ -1119,7 +1153,7 @@ public function dataUnion(): array [ [ new MixedType(), - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), ], MixedType::class, 'mixed=implicit', @@ -1130,7 +1164,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1143,7 +1177,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1156,13 +1190,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, @@ -1174,18 +1208,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()]), @@ -1271,7 +1411,7 @@ public function dataUnion(): array [ [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), ], ClassStringType::class, 'class-string', @@ -1294,15 +1434,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, @@ -1310,7 +1450,7 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), ], StringType::class, @@ -1318,64 +1458,64 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), ], UnionType::class, 'class-string|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], UnionType::class, '\'Exception\'|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], UnionType::class, '\'stdClass\'|class-string', @@ -1394,7 +1534,7 @@ public function dataUnion(): array new ConstantStringType('test_function'), ], UnionType::class, - '\'test_function\'|(callable(): mixed&string)', + '\'test_function\'|callable-string', ], [ [ @@ -1402,7 +1542,7 @@ public function dataUnion(): array new IntegerType(), ], UnionType::class, - '(callable(): mixed&string)|int', + 'callable-string|int', ], [ [ @@ -1436,6 +1576,34 @@ public function dataUnion(): array 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), @@ -1498,10 +1666,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, @@ -1510,10 +1678,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, @@ -1522,10 +1690,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, @@ -1537,7 +1705,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectWithoutClassType(), ], @@ -1550,9 +1718,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)', @@ -1563,7 +1731,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -1576,13 +1744,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, @@ -1594,13 +1762,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, @@ -1611,14 +1779,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, @@ -1626,16 +1794,16 @@ 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, ], [ [ new StringType(), - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), ], MixedType::class, 'mixed=implicit', @@ -1650,7 +1818,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array()|array(string)', + 'array{}|array{string}', ], [ [ @@ -1659,10 +1827,10 @@ public function dataUnion(): array new ConstantIntegerType(0), ], [ new StringType(), - ], 1, [0]), + ], [1], [0]), ], UnionType::class, - 'array()|array(?0 => string)', + 'array{}|array{0?: string}', ], [ [ @@ -1682,7 +1850,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}', ], [ [ @@ -1700,7 +1868,7 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'a\' => int, ?\'b\' => int)', + 'array{a: int, b?: int}', ], [ [ @@ -1720,23 +1888,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array(\'a\' => int, \'b\' => int)|array(\'b\' => int, \'c\' => int)', - ], - [ - [ - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - 'string&hasOffset(int)', - ], - [ - [ - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - '\'abc\'&hasOffset(int)', + 'array{a: int, b: int}|array{b: int, c: int}', ], [ [ @@ -1744,7 +1896,7 @@ public function dataUnion(): array StaticTypeFactory::falsey(), ], UnionType::class, - '0|0.0|\'\'|\'0\'|array()|false|null', + '0|0.0|\'\'|\'0\'|array{}|false|null', ], [ [ @@ -1752,7 +1904,7 @@ public function dataUnion(): array StaticTypeFactory::truthy(), ], MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array()|false|null=implicit', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', ], [ [ @@ -1780,6 +1932,28 @@ public function dataUnion(): array StringType::class, 'string', ], + [ + [ + new ConstantStringType('0'), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), + ], + IntersectionType::class, + 'non-empty-string', + ], + [ + [ + new ConstantStringType(''), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), + ], + StringType::class, + 'string', + ], [ [ new StringType(), @@ -1804,6 +1978,102 @@ public function dataUnion(): array UnionType::class, 'string|false', ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + ], + UnionType::class, + 'literal-string|numeric-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'literal-string|lowercase-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'numeric-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-falsy-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-empty-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'literal-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -1815,26 +2085,726 @@ public function dataUnion(): array new FloatType(), new BooleanType(), ]), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new NullType(), ], UnionType::class, '(T of bool|float|int|string (function doFoo(), parameter))|null', ], + [ + [ + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), + ], + UnionType::class, + 'array|int|int<1, max>|(TCode of array|int (class Foo, parameter))', + ], + [ + [ + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new CallableType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), + ], + UnionType::class, + 'array|(callable(): mixed)|(TCode of array|int (class Foo, parameter))', + ], + [ + [ + new MixedType(), + new StrictMixedType(), + ], + MixedType::class, + 'mixed=implicit', + ], ]; - } - /** - * @dataProvider dataUnion - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription - */ - public function testUnion( + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|PHPStan\Fixture\TestEnum::TWO', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectWithoutClassType(), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new ObjectType('stdClass'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|stdClass', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new MixedType(subtractedType: IntegerRangeType::fromInterval(17, null)), + IntegerRangeType::fromInterval(19, null), + ], + MixedType::class, + 'mixed~int<17, 18>=implicit', + ]; + + $reflectionProvider = self::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)], + optionalKeys: [0, 1], + ), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + optionalKeys: [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)], + optionalKeys: [0, 1], + ), + new NonEmptyArrayType(), + ]), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + optionalKeys: [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), + ], optionalKeys: [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], optionalKeys: [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), + ], optionalKeys: [0]), + new IntersectionType([ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], optionalKeys: [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(isPure: TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new CallableType(isPure: TrinaryLogic::createYes()), + ClosureType::createPure(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(isPure: TrinaryLogic::createMaybe()), + new CallableType(isPure: TrinaryLogic::createYes()), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), impurePoints: [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), + ], + UnionType::class, + '(Closure(): mixed)|(pure-Closure)', + ]; + yield [ + [ + new ClosureType([], new MixedType(), impurePoints: [ + 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(), classReflection: $finalClass), + new ObjectType($nonFinalClass->getName(), classReflection: $nonFinalClass), + ], + ObjectType::class, + $nonFinalClass->getDisplayName(), + ]; + } + + /** + * @param Type[] $types + * @param class-string $expectedTypeClass + */ + #[DataProvider('dataUnion')] + public function testUnion( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::union(...$types); @@ -1846,16 +2816,24 @@ public function testUnion( $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 function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, - $types - ))) + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), ); $this->assertInstanceOf($expectedTypeClass, $actualType); @@ -1875,15 +2853,14 @@ static function (Type $type): string { } /** - * @dataProvider dataUnion - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ + #[DataProvider('dataUnion')] public function testUnionInversed( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $types = array_reverse($types); @@ -1896,24 +2873,32 @@ public function testUnionInversed( $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 function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, - $types - ))) + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), ); $this->assertInstanceOf($expectedTypeClass, $actualType); } - public function dataIntersect(): array + public static function dataIntersect(): iterable { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); - return [ + yield from [ [ [ new IterableType(new MixedType(), new StringType()), @@ -1947,8 +2932,8 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('_'), 'T', null, - TemplateTypeVariance::createInvariant() - ) + TemplateTypeVariance::createInvariant(), + ), ), ], IntersectionType::class, @@ -2024,7 +3009,7 @@ public function dataIntersect(): array StaticTypeFactory::truthy(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2032,7 +3017,7 @@ public function dataIntersect(): array new NeverType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2091,7 +3076,7 @@ public function dataIntersect(): array new IterableType(new StringType(), new MixedType()), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2099,7 +3084,7 @@ public function dataIntersect(): array new IterableType(new MixedType(), new StringType()), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2138,929 +3123,1643 @@ public function dataIntersect(): array new ConstantIntegerType(1), new BenevolentUnionType([new IntegerType(), new StringType()]), ], - ConstantIntegerType::class, - '1', + 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 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 ConstantStringType('foo'), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new ClosureType([], new MixedType(), false), + new ObjectWithoutClassType(), ], - ConstantStringType::class, - '\'foo\'', + ClosureType::class, + 'Closure(): mixed', ], [ [ - new StringType(), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), ], - StringType::class, - 'string', + IntersectionType::class, + 'non-empty-array', ], [ [ - new UnionType([new StringType(), new IntegerType()]), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new StringType(), + new NonEmptyArrayType(), ], - UnionType::class, - '(int|string)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ObjectType(\Test\Foo::class), - new HasMethodType('__toString'), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + new NonEmptyArrayType(), ], IntersectionType::class, - 'Test\Foo&hasMethod(__toString)', + 'non-empty-array', ], [ [ - new ObjectType(\Test\ClassWithToString::class), - new HasMethodType('__toString'), + TypeCombinator::union( + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantIntegerType(0), + ], [ + new StringType(), + ]), + ), + new NonEmptyArrayType(), ], - ObjectType::class, - 'Test\ClassWithToString', + ConstantArrayType::class, + 'array{string}', ], [ [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithMethodExists::class), - new HasMethodType('doBar'), + new ConstantArrayType([], []), + new NonEmptyArrayType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), + 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, - 'object&hasMethod(__toString)', + 'non-empty-array&hasOffset(\'bar\')&hasOffset(\'foo\')', ], [ [ + new StringType(), new IntegerType(), - new HasMethodType('__toString'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), - ]), - new HasMethodType('__toString'), + new MixedType(subtractedType: new StringType()), + new StringType(), ], - IntersectionType::class, - 'object&hasMethod(__toString)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('foo'), - ]), - new HasMethodType('bar'), + new MixedType(subtractedType: new StringType()), + new ConstantStringType('foo'), ], - IntersectionType::class, - 'object&hasMethod(bar)&hasMethod(foo)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), - ]), - new HasMethodType('__toString'), + new MixedType(subtractedType: new StringType()), + new ConstantIntegerType(1), ], - UnionType::class, - '(Test\FirstInterface&hasMethod(__toString))|(Test\Foo&hasMethod(__toString))', + ConstantIntegerType::class, + '1', ], [ [ - new ObjectType(\Test\Foo::class), - new HasPropertyType('fooProperty'), + new MixedType(subtractedType: new StringType()), + new MixedType(subtractedType: new IntegerType()), + ], + MixedType::class, + 'mixed~(int|string)=implicit', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), ], IntersectionType::class, - 'Test\Foo&hasProperty(fooProperty)', + 'DateTime&T (function a(), parameter)', ], [ [ - new ObjectType(\Test\ClassWithNullableProperty::class), - new HasPropertyType('foo'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), ], - ObjectType::class, - 'Test\ClassWithNullableProperty', + TemplateObjectType::class, + 'T of DateTime (function a(), parameter)', ], [ [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithPropertyExists::class), - new HasPropertyType('barProperty'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), ], - NeverType::class, - '*NEVER*', + TemplateType::class, + 'T of DateTime (function a(), parameter)', ], [ [ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'U', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), ], IntersectionType::class, - 'object&hasProperty(fooProperty)', + 'T of DateTime (function a(), parameter)&U of DateTime (function a(), parameter)', ], [ [ - new IntegerType(), - new HasPropertyType('fooProperty'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new MixedType(), ], - NeverType::class, - '*NEVER*', + TemplateType::class, + 'T (function a(), parameter)=explicit', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), - ]), - new HasPropertyType('fooProperty'), + new StringType(), + new ClassStringType(), ], - IntersectionType::class, - 'object&hasProperty(fooProperty)', + ClassStringType::class, + 'class-string', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('foo'), - ]), - new HasPropertyType('bar'), + new ClassStringType(), + new ConstantStringType(stdClass::class), ], - IntersectionType::class, - 'object&hasProperty(bar)&hasProperty(foo)', + ConstantStringType::class, + '\'stdClass\'', ], [ [ - new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), - ]), - new HasPropertyType('fooProperty'), + new ClassStringType(), + new ConstantStringType('Nonexistent'), ], - UnionType::class, - '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), + new ClassStringType(), + new IntegerType(), ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), - new HasOffsetType(new ConstantStringType('a')), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + ConstantStringType::class, + '\'Exception\'', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), + new GenericClassStringType(new ObjectType(Exception::class)), + new ClassStringType(), ], - IntersectionType::class, - 'array&hasOffset(string)', + GenericClassStringType::class, + 'class-string', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), + new GenericClassStringType(new ObjectType(Exception::class)), + new StringType(), ], - IntersectionType::class, - 'array&hasOffset(string)', + GenericClassStringType::class, + 'class-string', ], [ [ - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')] - ), - new HasOffsetType(new ConstantStringType('a')), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], - ConstantArrayType::class, - 'array(\'a\' => \'foo\')', + GenericClassStringType::class, + 'class-string', ], [ [ - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')] - ), - new HasOffsetType(new ConstantStringType('b')), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], - NeverType::class, - '*NEVER*', + GenericClassStringType::class, + 'class-string', ], [ [ - new ClosureType([], new MixedType(), false), - new HasOffsetType(new ConstantStringType('a')), + 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*', + '*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')), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], - ConstantArrayType::class, - 'array(\'b\' => \'foo\')', + ConstantStringType::class, + '\'Exception\'', ], [ [ - TypeCombinator::union( - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')] - ), - new ClosureType([], new MixedType(), false) - ), - new HasOffsetType(new ConstantStringType('a')), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], - ConstantArrayType::class, - 'array(\'a\' => \'foo\')', + ConstantStringType::class, + '\'Exception\'', ], [ [ - new ClosureType([], new MixedType(), false), - new ObjectType(\Closure::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], - ClosureType::class, - 'Closure(): mixed', + IntersectionType::class, + "'Exception'&class-string", ], [ [ - new ClosureType([], new MixedType(), false), - new CallableType(), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], - ClosureType::class, - 'Closure(): mixed', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ClosureType([], new MixedType(), false), - new ObjectWithoutClassType(), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(2, 5), ], - ClosureType::class, - 'Closure(): mixed', + IntegerRangeType::class, + 'int<2, 3>', ], [ [ - new UnionType([ - new ArrayType(new MixedType(), new StringType()), - new NullType(), - ]), - new HasOffsetType(new StringType()), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(3, 5), ], - IntersectionType::class, - 'array&hasOffset(string)', + ConstantIntegerType::class, + '3', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType(), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(7, 9), ], - IntersectionType::class, - 'array&nonEmpty', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new StringType(), - new NonEmptyArrayType(), + IntegerRangeType::fromInterval(1, 3), + new ConstantIntegerType(3), ], - NeverType::class, - '*NEVER*', + ConstantIntegerType::class, + '3', ], [ [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType(), - ]), - new NonEmptyArrayType(), + IntegerRangeType::fromInterval(1, 3), + new ConstantIntegerType(4), ], - IntersectionType::class, - 'array&nonEmpty', + NeverType::class, + '*NEVER*=implicit', ], [ [ - TypeCombinator::union( - new ConstantArrayType([], []), - new ConstantArrayType([ - new ConstantIntegerType(0), - ], [ - new StringType(), - ]) - ), - new NonEmptyArrayType(), + IntegerRangeType::fromInterval(1, 3), + new IntegerType(), ], - ConstantArrayType::class, - 'array(string)', + IntegerRangeType::class, + 'int<1, 3>', ], [ [ - new ConstantArrayType([], []), - new NonEmptyArrayType(), + new ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType()), ], - NeverType::class, - '*NEVER*', + ObjectType::class, + 'Traversable', ], [ [ - 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 ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType()), ], - IntersectionType::class, - 'array&hasOffset(\'bar\')&hasOffset(\'foo\')', + ObjectType::class, + 'Traversable', ], [ [ - new StringType(), - new IntegerType(), + new ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType(true)), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - new MixedType(false, new StringType()), - new StringType(), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new MixedType()), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - new MixedType(false, new StringType()), - new ConstantStringType('foo'), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new MixedType(true)), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - new MixedType(false, new StringType()), - new ConstantIntegerType(1), + new MixedType(), + new MixedType(), ], - ConstantIntegerType::class, - '1', + MixedType::class, + 'mixed=implicit', ], [ [ - new MixedType(false, new StringType()), - new MixedType(false, new IntegerType()), + new MixedType(true), + new MixedType(), ], MixedType::class, - 'mixed~int|string=implicit', + 'mixed=explicit', ], [ [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - null, - TemplateTypeVariance::createInvariant() - ), - new ObjectType('DateTime'), + new MixedType(true), + new MixedType(true), ], - IntersectionType::class, - 'DateTime&T (function a(), parameter)', + MixedType::class, + 'mixed=explicit', ], [ [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() - ), - new ObjectType('DateTime'), + new GenericObjectType(Variance\Covariant::class, [ + new ObjectType(DateTimeInterface::class), + ]), + new GenericObjectType(Variance\Covariant::class, [ + new ObjectType(DateTime::class), + ]), ], - TemplateObjectType::class, - 'T of DateTime (function a(), parameter)', + GenericObjectType::class, + 'PHPStan\Type\Variance\Covariant', ], [ [ TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() - ), - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), ), + new ObjectWithoutClassType(), ], - TemplateType::class, - 'T of DateTime (function a(), parameter)', + TemplateObjectWithoutClassType::class, + 'T of object (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() + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), ), + new ObjectType(stdClass::class), ], IntersectionType::class, - 'T of DateTime (function a(), parameter)&U of DateTime (function a(), parameter)', + 'stdClass&T of object (function a(), parameter)', ], [ [ TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', - null, - TemplateTypeVariance::createInvariant() + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], - TemplateType::class, - 'T (function a(), parameter)=explicit', + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', ], [ [ - new StringType(), + new ConstantStringType('NonexistentClass'), new ClassStringType(), ], - ClassStringType::class, - 'class-string', + NeverType::class, + '*NEVER*=implicit', ], [ [ + new ConstantStringType(stdClass::class), new ClassStringType(), - new ConstantStringType(\stdClass::class), ], ConstantStringType::class, '\'stdClass\'', ], [ [ - new ClassStringType(), - new ConstantStringType('Nonexistent'), + new ObjectType(DateTimeInterface::class), + new ObjectType(Iterator::class), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'DateTimeInterface&Iterator', ], [ [ - new ClassStringType(), - new IntegerType(), + new ObjectType(DateTimeInterface::class), + new GenericObjectType(Iterator::class, [new MixedType(), new MixedType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'DateTimeInterface&Iterator', ], [ [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0]), + new HasOffsetType(new ConstantStringType('a')), ], - ConstantStringType::class, - '\'Exception\'', + ConstantArrayType::class, + 'array{a: int, b: int}', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ClassStringType(), + new BenevolentUnionType([new IntegerType(), new StringType()]), + new MixedType(), ], - GenericClassStringType::class, - 'class-string', + BenevolentUnionType::class, + '(int|string)', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new StringType(), + new ConstantStringType('abc'), + new AccessoryNumericStringType(), ], - GenericClassStringType::class, - 'class-string', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType('123'), + new AccessoryNumericStringType(), ], - GenericClassStringType::class, - 'class-string', + ConstantStringType::class, + '\'123\'', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new StringType(), + new AccessoryNumericStringType(), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'numeric-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new IntegerType(), + new AccessoryNumericStringType(), ], - GenericClassStringType::class, - 'class-string', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new IntersectionType([ + new ArrayType(new StringType(), new IntegerType()), + new NonEmptyArrayType(), + ]), + new NeverType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new UnionType([new IntegerType(), new StringType()]), + ], + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new UnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new UnionType([new IntegerType(), new StringType()]), ], - ConstantStringType::class, - '\'Exception\'', + UnionType::class, + 'T of int|string (function my_array_keys(), parameter)', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new MixedType(), + new StrictMixedType(), ], - ConstantStringType::class, - '\'Exception\'', + StrictMixedType::class, + 'mixed', ], [ [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new NeverType(true), + new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=explicit', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::class), + new NeverType(), + new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(2, 5), + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - IntegerRangeType::class, - 'int<2, 3>', + IntersectionType::class, + 'lowercase-string', ], [ [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(3, 5), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantIntegerType::class, - '3', + IntersectionType::class, + 'lowercase-string&numeric-string', ], [ [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(7, 9), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'lowercase-string&non-falsy-string', ], [ [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(3), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantIntegerType::class, - '3', + IntersectionType::class, + 'lowercase-string&non-empty-string', ], [ [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(4), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'literal-string&lowercase-string', ], [ [ - IntegerRangeType::fromInterval(1, 3), - new IntegerType(), + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - IntegerRangeType::class, - 'int<1, 3>', + IntersectionType::class, + 'uppercase-string', ], [ [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType()), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ObjectType::class, - 'Traversable', + IntersectionType::class, + 'numeric-string&uppercase-string', ], [ [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType()), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ObjectType::class, - 'Traversable', + IntersectionType::class, + 'non-falsy-string&uppercase-string', ], [ [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType(true)), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], IntersectionType::class, - 'iterable&Traversable', + 'non-empty-string&uppercase-string', ], [ [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new MixedType()), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], IntersectionType::class, - 'iterable&Traversable', + 'literal-string&uppercase-string', ], [ [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new MixedType(true)), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], IntersectionType::class, - 'iterable&Traversable', + 'lowercase-string&uppercase-string', ], [ [ - new MixedType(), - new MixedType(), + new ObjectType(DateTime::class), + new MixedType(subtractedType: new NullType()), ], - MixedType::class, - 'mixed=implicit', + ObjectType::class, + 'DateTime', ], [ [ - new MixedType(true), - new MixedType(), + new ObjectWithoutClassType(), + new MixedType(subtractedType: new NullType()), ], - MixedType::class, - 'mixed=explicit', + ObjectWithoutClassType::class, + 'object', ], [ [ - new MixedType(true), - new MixedType(true), + new MixedType(subtractedType: new ObjectWithoutClassType(new ObjectType('stdClass'))), + new ObjectWithoutClassType(), ], - MixedType::class, - 'mixed=explicit', + ObjectType::class, + 'stdClass', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType(stdClass::class, 'ONE'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectType(FinalClass::class), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectWithoutClassType(), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('stdClass'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new MixedType(subtractedType: 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(subtractedType: 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(subtractedType: 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), + ], optionalKeys: [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], optionalKeys: [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), + ], optionalKeys: [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), + ], optionalKeys: [0, 1]), + new NonEmptyArrayType(), ], + IntersectionType::class, + 'non-empty-array{a?: true, c?: true}', + ]; + 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 CallableType(isPure: TrinaryLogic::createYes()), + new CallableType(), ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() - ), - new ObjectWithoutClassType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + new CallableType(isPure: TrinaryLogic::createYes()), + ClosureType::createPure(), ], + ClosureType::class, + 'pure-Closure', + ]; + 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 CallableType(isPure: TrinaryLogic::createMaybe()), + new CallableType(isPure: TrinaryLogic::createYes()), ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() - ), - new MixedType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + new ClosureType([], new MixedType(), impurePoints: [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ConstantStringType('NonexistentClass'), - new ClassStringType(), - ], - NeverType::class, - '*NEVER*', + new ClosureType([], new MixedType(), impurePoints: [ + 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 ConstantStringType(\stdClass::class), - new ClassStringType(), - ], - ConstantStringType::class, - '\'stdClass\'', + new UnionType([ + new ConstantArrayType([], []), + $xy, + $abxy, + ]), + new UnionType([ + $xy, + $abxy, + ]), ], + UnionType::class, + "array{'xy'}|array{0: 'ab', 1?: 'xy'}", + ]; + + yield [ [ - [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\Iterator::class), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new ConstantArrayType([], []), + new UnionType([ + $xy, + $abxy, + ]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - new ObjectType(\DateTimeInterface::class), - new GenericObjectType(\Iterator::class, [new MixedType(), new MixedType()]), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new ConstantArrayType([], []), + $abxy, ], + NeverType::class, + '*NEVER*=implicit', + ]; + + 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 ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new StringType(), - new HasOffsetType(new IntegerType()), - ], - IntersectionType::class, - 'string&hasOffset(int)', + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], + ConstantStringType::class, + '\'foo\'', + ]; + + yield [ [ - [ - new BenevolentUnionType([new IntegerType(), new StringType()]), - new MixedType(), - ], - BenevolentUnionType::class, - '(int|string)', + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ConstantStringType('abc'), - new AccessoryNumericStringType(), - ], - NeverType::class, - '*NEVER*', + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], + ConstantStringType::class, + '\'FOO\'', + ]; + + $c = $reflectionProvider->getClass(C::class); + + yield [ [ - [ - new ConstantStringType('123'), - new AccessoryNumericStringType(), - ], - ConstantStringType::class, - '\'123\'', + 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 StringType(), - new AccessoryNumericStringType(), - ], - IntersectionType::class, - 'string&numeric', + new GenericStaticType($c, [new StringType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new UnionType([ new IntegerType(), - new AccessoryNumericStringType(), - ], - NeverType::class, - '*NEVER*', + new StringType(), + ])], null, [TemplateTypeVariance::createCovariant()]), ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ [ - [ - new IntersectionType([ - new ArrayType(new StringType(), new IntegerType()), - new NonEmptyArrayType(), - ]), - new NeverType(), - ], - NeverType::class, - '*NEVER*', + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() - ), - new UnionType([new IntegerType(), new StringType()]), - ], - TemplateBenevolentUnionType::class, - 'T of (int|string) (function my_array_keys(), parameter)', + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectWithoutClassType(), ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() - ), - new BenevolentUnionType([new IntegerType(), new StringType()]), - ], - TemplateBenevolentUnionType::class, - 'T of (int|string) (function my_array_keys(), parameter)', + new 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 [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new UnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() - ), - new UnionType([new IntegerType(), new StringType()]), - ], - UnionType::class, - 'T of int|string (function my_array_keys(), parameter)', + new ObjectType($finalClass->getName(), classReflection: $finalClass), + new ObjectType($nonFinalClass->getName(), classReflection: $nonFinalClass), ], + ObjectType::class, + $nonFinalClass->getDisplayName() . '=final', ]; } /** - * @dataProvider dataIntersect - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ + #[DataProvider('dataIntersect')] public function testIntersect( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...$types); @@ -3072,20 +4771,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 class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ + #[DataProvider('dataIntersect')] public function testIntersectInversed( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...array_reverse($types)); @@ -3097,18 +4814,36 @@ 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); } - public function dataRemove(): array + public static function dataRemove(): array { return [ [ new ConstantBooleanType(true), new ConstantBooleanType(true), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new UnionType([ @@ -3164,13 +4899,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(), @@ -3188,31 +4923,31 @@ public function dataRemove(): array new BooleanType(), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::falsey(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), StaticTypeFactory::truthy(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), StaticTypeFactory::falsey(), MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array()|false|null', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::truthy(), UnionType::class, - '0|0.0|\'\'|\'0\'|array()|false|null', + '0|0.0|\'\'|\'0\'|array{}|false|null', ], [ new BooleanType(), @@ -3283,13 +5018,13 @@ public function dataRemove(): array ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), ArrayType::class, 'array', ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Iterator::class), + new ObjectType(Iterator::class), IterableType::class, 'iterable', ], @@ -3309,25 +5044,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|int<2, max>|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( @@ -3336,11 +5071,11 @@ public function dataRemove(): array new ConstantIntegerType(0), ], [ new StringType(), - ]) + ]), ), new ConstantArrayType([], []), ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ new IntersectionType([ @@ -3349,13 +5084,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()), @@ -3373,37 +5108,37 @@ public function dataRemove(): array 'mixed~int', ], [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new IntegerType(), MixedType::class, 'mixed~int', ], [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: 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(subtractedType: new StringType()), new MixedType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(false), - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), StringType::class, 'string', ], [ - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), new NeverType(), MixedType::class, 'mixed~string', @@ -3418,13 +5153,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')), @@ -3466,25 +5201,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), @@ -3514,10 +5249,10 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2), + ], [2]), new HasOffsetType(new ConstantIntegerType(1)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ConstantArrayType([ @@ -3526,10 +5261,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([ @@ -3538,10 +5273,10 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(0)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(), @@ -3556,30 +5291,70 @@ public function dataRemove(): array 'object', ], [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new NeverType(), ObjectType::class, 'stdClass', ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new BooleanType(), + TemplateTypeVariance::createInvariant(), + ), + new ConstantBooleanType(false), + TemplateMixedType::class, // should be TemplateConstantBooleanType + 'T (class Foo, parameter)', // should be T of true + ], + [ + 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 class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ + #[DataProvider('dataRemove')] 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); } @@ -3599,7 +5374,23 @@ 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 static 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..e7e0ae2de6 --- /dev/null +++ b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php @@ -0,0 +1,146 @@ + $expectedTypes + */ + #[DataProvider('dataGetFiniteTypes')] + 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..29bfe8f70a --- /dev/null +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -0,0 +1,566 @@ +', + ]; + + 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), + ], optionalKeys: [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'), + ], optionalKeys: [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(), + ], variances: [ + 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 static 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 static function dataFromTypeStringToPhpDocNode(): iterable + { + foreach (self::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 2226b18e20..d0ec27f619 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -2,9 +2,15 @@ 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; @@ -15,25 +21,34 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; - -class UnionTypeTest extends \PHPStan\Testing\PHPStanTestCase +use PHPUnit\Framework\Attributes\DataProvider; +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 + public static function dataIsCallable(): array { return [ [ 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(), ], @@ -61,24 +76,23 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - * @param UnionType $type - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataSelfCompare(): \Iterator + /** + * @return Iterator + */ + public static function dataSelfCompare(): Iterator { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $integerType = new IntegerType(); $stringType = new StringType(); @@ -99,14 +113,14 @@ public function dataSelfCompare(): \Iterator yield [new CallableType([$mixedParam, $integerParam], $stringType, false)]; yield [new ClassStringType()]; yield [new ClosureType([$mixedParam, $integerParam], $stringType, false)]; - yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], 10, [1])]; + yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], [10], [1])]; yield [new ConstantBooleanType(true)]; yield [new ConstantFloatType(3.14)]; yield [$constantIntegerType]; yield [$constantStringType]; yield [new ErrorType()]; yield [new FloatType()]; - yield [new GenericClassStringType(new ObjectType(\Exception::class))]; + yield [new GenericClassStringType(new ObjectType(Exception::class))]; yield [new GenericObjectType('Foo', [new ObjectType('DateTime')])]; yield [new HasMethodType('Foo')]; yield [new HasOffsetType($constantStringType)]; @@ -135,31 +149,30 @@ public function dataSelfCompare(): \Iterator yield [new VoidType()]; } - /** - * @dataProvider dataSelfCompare - * - * @param Type $type - */ + #[DataProvider('dataSelfCompare')] public function testSelfCompare(Type $type): void { $description = $type->describe(VerbosityLevel::precise()); $this->assertTrue( $type->equals($type), - sprintf('%s -> equals(itself)', $description) + sprintf('%s -> equals(itself)', $description), ); - $this->assertEquals( + $this->assertSame( 'Yes', $type->isSuperTypeOf($type)->describe(), - sprintf('%s -> isSuperTypeOf(itself)', $description) + sprintf('%s -> isSuperTypeOf(itself)', $description), ); $this->assertInstanceOf( get_class($type), TypeCombinator::union($type, $type), - sprintf('%s -> union with itself is same type', $description) + sprintf('%s -> union with itself is same type', $description), ); } - public function dataIsSuperTypeOf(): \Iterator + /** + * @return Iterator + */ + public static function dataIsSuperTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -308,7 +321,7 @@ public function dataIsSuperTypeOf(): \Iterator yield [ $unionTypeB, - new ObjectType('Foo'), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ]; @@ -335,25 +348,128 @@ 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(UnionType $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())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + /** + * @return Iterator + */ + public static function dataIsSubTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -503,55 +619,112 @@ 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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); $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 - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); $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 dataDescribe(): array + public static function dataIsScalar(): array + { + return [ + [ + TypeCombinator::union( + new BooleanType(), + new IntegerType(), + new FloatType(), + new StringType(), + ), + TrinaryLogic::createYes(), + ], + [ + new UnionType([ + new BooleanType(), + new ObjectType(DateTimeImmutable::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new FloatType(), + new MixedType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new StringType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new NullType(), + new ObjectType(DateTimeImmutable::class), + new ResourceType(), + ]), + TrinaryLogic::createNo(), + ], + ]; + } + + #[DataProvider('dataIsScalar')] + public function testIsScalar(UnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isScalar(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isScalar()', $type->describe(VerbosityLevel::precise())), + ); + } + + public static function dataDescribe(): array { return [ [ new UnionType([new IntegerType(), new StringType()]), 'int|string', 'int|string', + 'int|string', + 'int|string', ], [ new UnionType([new IntegerType(), new StringType(), new NullType()]), 'int|string|null', 'int|string|null', + 'int|string|null', + 'int|string|null', ], [ new UnionType([ @@ -565,12 +738,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-PHPStan\Type\ObjectType-|true|null", + "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', ], @@ -590,9 +765,11 @@ 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}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', 'array|string', ], [ @@ -611,9 +788,11 @@ 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}', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', 'array|string', ], [ @@ -632,9 +811,11 @@ 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}', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', 'array|string', ], [ @@ -652,9 +833,11 @@ 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{int, bool, float}|array{string}', 'array', ], [ @@ -664,9 +847,11 @@ public function dataDescribe(): array new ConstantStringType('foooo'), ], [ new ConstantStringType('barrr'), - ]) + ]), ), - 'array()|array(\'foooo\' => \'barrr\')', + 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -675,33 +860,116 @@ public function dataDescribe(): array new IntersectionType([ new StringType(), new AccessoryNumericStringType(), - ]) + ]), ), - 'int|(string&numeric)', + 'int|numeric-string', + '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>', + '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 (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 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 (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 (class foo, parameter) (class foo, parameter)|null', + '(TFoo of TBar)|null', + '(TFoo of TBar)|null', + ], + [ + new UnionType([new ObjectType('Foo'), new ObjectType('Foo')]), + 'Foo-PHPStan\Type\ObjectType-#1|Foo-PHPStan\Type\ObjectType-#2', + 'Foo#1|Foo#2', + 'Foo', + 'Foo', + ], ]; } - /** - * @dataProvider dataDescribe - * @param Type $type - * @param string $expectedValueDescription - * @param string $expectedTypeOnlyDescription - */ + #[DataProvider('dataDescribe')] public function testDescribe( Type $type, + string $expectedCacheDescription, + string $expectedPreciseDescription, string $expectedValueDescription, - string $expectedTypeOnlyDescription + string $expectedTypeOnlyDescription, ): void { - $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::precise())); + $this->assertSame($expectedCacheDescription, $type->describe(VerbosityLevel::cache())); + $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 static function dataAccepts(): iterable { - return [ + yield from [ [ new UnionType([new CallableType(), new NullType()]), new ClosureType([], new StringType(), false), @@ -769,38 +1037,301 @@ 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 - */ + #[DataProvider('dataAccepts')] 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())), ); } - public function dataHasMethod(): array + public static function dataHasMethod(): array { return [ [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new IntegerType()]), + new UnionType([new ObjectType(DateTimeImmutable::class), new IntegerType()]), 'format', TrinaryLogic::createMaybe(), ], [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new ObjectType(\DateTime::class)]), + new UnionType([new ObjectType(DateTimeImmutable::class), new ObjectType(DateTime::class)]), 'format', TrinaryLogic::createYes(), ], @@ -810,23 +1341,18 @@ 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(), ], ]; } - /** - * @dataProvider dataHasMethod - * @param UnionType $type - * @param string $methodName - * @param TrinaryLogic $expectedResult - */ + #[DataProvider('dataHasMethod')] public function testHasMethod( UnionType $type, string $methodName, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame($expectedResult->describe(), $type->hasMethod($methodName)->describe()); @@ -860,10 +1386,268 @@ public function testSorting(): void $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' + 'UnionType sorting always produces the same order', ); } + /** + * @param Type[] $types + * @param list $expectedDescriptions + */ + #[DataProvider('dataGetConstantArrays')] + 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 static 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(), + ], + [], + ], + ]; + } + + /** + * @param list $expectedDescriptions + */ + #[DataProvider('dataGetConstantStrings')] + 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 static 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'", + ], + ], + ]; + } + + /** + * @param list $expectedObjectClassNames + */ + #[DataProvider('dataGetObjectClassNames')] + public function testGetObjectClassNames( + Type $unionType, + array $expectedObjectClassNames, + ): void + { + $this->assertSame($expectedObjectClassNames, $unionType->getObjectClassNames()); + } + + public static 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(), + ), + [], + ], + ]; + } + + /** + * @param list $expectedDescriptions + */ + #[DataProvider('dataGetArrays')] + 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 static 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..5297e6505f --- /dev/null +++ b/tests/PHPStan/Type/VerbosityLevelTest.php @@ -0,0 +1,56 @@ +assertSame($expected->getLevelValue(), $level->getLevelValue()); + } + +} diff --git a/tests/PHPStan/Type/data/ExtendsThrowable.php b/tests/PHPStan/Type/data/ExtendsThrowable.php new file mode 100644 index 0000000000..035bf18cef --- /dev/null +++ b/tests/PHPStan/Type/data/ExtendsThrowable.php @@ -0,0 +1,10 @@ += 8.1 + +namespace ObjectTypeEnums; + +enum FooEnum +{ + + case FOO; + case BAR; + case BAZ; + +} diff --git a/tests/PHPStan/Type/data/annotations.php b/tests/PHPStan/Type/data/annotations.php index 0732fd31c4..bf157ab365 100644 --- a/tests/PHPStan/Type/data/annotations.php +++ b/tests/PHPStan/Type/data/annotations.php @@ -1,5 +1,7 @@ | 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/data/Foo.php b/tests/PHPStan/data/Foo.php new file mode 100644 index 0000000000..8dc2961b07 --- /dev/null +++ b/tests/PHPStan/data/Foo.php @@ -0,0 +1,7 @@ +=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.3" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-07-05T12:25:42+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "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.5.0" + }, + "time": "2025-05-31T08:24:38+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": "11.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.2" + }, + "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": "11.0.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/show" + }, + "funding": [ + { + "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/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-06-18T08:56:18+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.3", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.2", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-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/11.5.27" + }, + "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-07-11T04:10:06+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": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "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", + "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", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-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", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T06:57:01+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-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": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "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/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-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", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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", + "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", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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": "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", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^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/v7.3.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": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.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.32.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.32.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.32.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.32.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.32.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.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "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.32.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-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "shasum": "" + }, + "require": { + "php": ">=8.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/v7.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": "2025-04-17T09:11:12+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "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": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "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.6.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": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "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.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": "2025-04-20T20:19:01+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": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.2.99" + }, + "plugin-api-version": "2.6.0" +} diff --git a/tests/dump-reflection-test-symbols.php b/tests/dump-reflection-test-symbols.php new file mode 100644 index 0000000000..17281662c6 --- /dev/null +++ b/tests/dump-reflection-test-symbols.php @@ -0,0 +1,10 @@ +&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(5, $result['totals']['file_errors'], Json::encode($result)); - $this->assertSame(0, $result['totals']['errors'], Json::encode($result)); - - $message = $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][0]['message']; - $this->assertStringContainsString('Ignored error pattern #^Argument of an invalid type PhpParser\\\\Node supplied for foreach, only iterables are supported\\.$# in path', $message); - $this->assertStringContainsString('was not matched in reported errors.', $message); - $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][1]['message']); - $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][2]['message']); - $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][3]['message']); - $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][4]['message']); - - file_put_contents($serializerPath, $originalSerializerCode); - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - } - - /** - * @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/baseline.neon b/tests/e2e/baseline.neon deleted file mode 100644 index a6bdfbe83c..0000000000 --- a/tests/e2e/baseline.neon +++ /dev/null @@ -1,192 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^PHPDoc tag @param references unknown parameter\\: \\$interfaces$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Class_.php - - - - message: "#^PHPDoc tag @param references unknown parameter\\: \\$interfaces$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Interface_.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$stmts\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Interface_.php - - - - message: "#^Result of && is always false\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Method PhpParser\\\\BuilderAbstract\\:\\:normalizeValue\\(\\) should return PhpParser\\\\Node\\\\Expr but returns PhpParser\\\\Node\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Access to an undefined property PhpParser\\\\BuilderAbstract\\:\\:\\$flags\\.$#" - count: 2 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^PHPDoc tag @param has invalid value \\(string\\|Node\\\\Name Name to alias\\)\\: Unexpected token \"Name\", expected variable at offset 88$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderFactory.php - - - - message: "#^Expression \"@\\$undefinedVariable\" on a separate line does not do anything\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Undefined variable\\: \\$undefinedVariable$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Empty array passed to foreach\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer/Emulative.php - - - - message: "#^Method PhpParser\\\\Node\\\\Expr\\\\Closure\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Expr/Closure.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 4 - path: PHP-Parser/lib/PhpParser/Node/Name.php - - - - message: "#^Method PhpParser\\\\Node\\\\Stmt\\\\ClassMethod\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\|null\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/ClassMethod.php - - - - message: "#^Method PhpParser\\\\Node\\\\Stmt\\\\Function_\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/Function_.php - - - - message: "#^PHPDoc tag @param for parameter \\$attributes with type array\\|null is not subtype of native type array\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/TryCatch.php - - - - message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$name\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$namespacedName\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:leaveNode\\(\\) should return array\\\\|int\\|PhpParser\\\\Node\\|false\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:afterTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\\\Expr\\:\\:\\$class\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\\\Expr\\:\\:\\$name\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Variable \\$s might not be defined\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Variable \\$s might not be defined\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/Parser/Php7.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributes \\(array\\) does not accept string\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributeStack \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Comparison operation \"\\<\" between \\(array\\|float\\|int\\<0, max\\>\\) and int results in an error\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Comparison operation \"\\>\\=\" between \\(array\\|float\\|int\\) and 0 results in an error\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Variable \\$tokenValue might not be defined\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Variable \\$action might not be defined\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Strict comparison using \\=\\=\\= between null and PhpParser\\\\Node will always evaluate to false\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php - - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php - - - - message: "#^Argument of an invalid type PhpParser\\\\Node supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Serializer/XML.php - diff --git a/tests/e2e/data/empty.neon b/tests/e2e/data/empty.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/data/soap.php b/tests/e2e/data/soap.php index e57afe1481..d75edf768e 100644 --- a/tests/e2e/data/soap.php +++ b/tests/e2e/data/soap.php @@ -14,9 +14,9 @@ class MySoapClient2 extends \SoapClient /** * @param string|null $wsdl - * @param mixed[]|null $options + * @param mixed[] $options */ - public function __construct($wsdl, array $options = null) + public function __construct($wsdl, array $options = []) { parent::__construct($wsdl, $options); } @@ -46,7 +46,7 @@ class MySoapHeader extends \SoapHeader public function __construct(string $username, string $password) { - parent::SoapHeader($username, $password); + parent::__construct($username, $password); } } diff --git a/tests/e2e/data/timecop.php b/tests/e2e/data/timecop.php index 3a0ee354a1..0c7ad015cb 100644 --- a/tests/e2e/data/timecop.php +++ b/tests/e2e/data/timecop.php @@ -20,4 +20,9 @@ public static function create(): self return new self(new DateTimeImmutable()); } + public function getBar(): DateTimeImmutable + { + return $this->bar; + } + } diff --git a/tests/e2e/phpstan.neon b/tests/e2e/phpstan.neon deleted file mode 100644 index 4bf004daf0..0000000000 --- a/tests/e2e/phpstan.neon +++ /dev/null @@ -1,5 +0,0 @@ -includes: - - baseline.neon - -parameters: - tmpDir: tmp diff --git a/tests/e2e/resultCache_1.php b/tests/e2e/resultCache_1.php deleted file mode 100644 index 8aabcb449b..0000000000 --- a/tests/e2e/resultCache_1.php +++ /dev/null @@ -1,2018 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeAbstract.php', - 173 => 'lib/PhpParser/NodeDumper.php', - 174 => 'lib/PhpParser/NodeTraverser.php', - 175 => 'lib/PhpParser/NodeTraverserInterface.php', - 176 => 'lib/PhpParser/NodeVisitor.php', - 177 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 178 => 'lib/PhpParser/NodeVisitorAbstract.php', - 179 => 'lib/PhpParser/Parser.php', - 180 => 'lib/PhpParser/Parser/Multiple.php', - 181 => 'lib/PhpParser/Parser/Php5.php', - 182 => 'lib/PhpParser/Parser/Php7.php', - 183 => 'lib/PhpParser/ParserAbstract.php', - 184 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 185 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 186 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Node/Arg.php', - 4 => 'lib/PhpParser/Node/Const_.php', - 5 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 7 => 'lib/PhpParser/Node/Expr/Array_.php', - 8 => 'lib/PhpParser/Node/Expr/Assign.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 22 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 51 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 52 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 53 => 'lib/PhpParser/Node/Expr/Cast.php', - 54 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 61 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 62 => 'lib/PhpParser/Node/Expr/Clone_.php', - 63 => 'lib/PhpParser/Node/Expr/Closure.php', - 64 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 65 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 66 => 'lib/PhpParser/Node/Expr/Empty_.php', - 67 => 'lib/PhpParser/Node/Expr/Error.php', - 68 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 69 => 'lib/PhpParser/Node/Expr/Eval_.php', - 70 => 'lib/PhpParser/Node/Expr/Exit_.php', - 71 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 72 => 'lib/PhpParser/Node/Expr/Include_.php', - 73 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 74 => 'lib/PhpParser/Node/Expr/Isset_.php', - 75 => 'lib/PhpParser/Node/Expr/List_.php', - 76 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 77 => 'lib/PhpParser/Node/Expr/New_.php', - 78 => 'lib/PhpParser/Node/Expr/PostDec.php', - 79 => 'lib/PhpParser/Node/Expr/PostInc.php', - 80 => 'lib/PhpParser/Node/Expr/PreDec.php', - 81 => 'lib/PhpParser/Node/Expr/PreInc.php', - 82 => 'lib/PhpParser/Node/Expr/Print_.php', - 83 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 84 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 85 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 86 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 87 => 'lib/PhpParser/Node/Expr/Ternary.php', - 88 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 90 => 'lib/PhpParser/Node/Expr/Variable.php', - 91 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 92 => 'lib/PhpParser/Node/Expr/Yield_.php', - 93 => 'lib/PhpParser/Node/Param.php', - 94 => 'lib/PhpParser/Node/Scalar.php', - 95 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 96 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 97 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 98 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 99 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 108 => 'lib/PhpParser/Node/Scalar/String_.php', - 109 => 'lib/PhpParser/Node/Stmt/Break_.php', - 110 => 'lib/PhpParser/Node/Stmt/Case_.php', - 111 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 112 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 113 => 'lib/PhpParser/Node/Stmt/Do_.php', - 114 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 115 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 116 => 'lib/PhpParser/Node/Stmt/For_.php', - 117 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 118 => 'lib/PhpParser/Node/Stmt/Global_.php', - 119 => 'lib/PhpParser/Node/Stmt/If_.php', - 120 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 121 => 'lib/PhpParser/Node/Stmt/Return_.php', - 122 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 123 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 124 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 125 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 126 => 'lib/PhpParser/Node/Stmt/While_.php', - 127 => 'lib/PhpParser/NodeDumper.php', - 128 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 129 => 'lib/PhpParser/Parser/Php5.php', - 130 => 'lib/PhpParser/Parser/Php7.php', - 131 => 'lib/PhpParser/ParserAbstract.php', - 132 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 133 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Node/Arg.php', - 13 => 'lib/PhpParser/Node/Const_.php', - 14 => 'lib/PhpParser/Node/Expr.php', - 15 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 17 => 'lib/PhpParser/Node/Expr/Array_.php', - 18 => 'lib/PhpParser/Node/Expr/Assign.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 32 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 61 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 62 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 63 => 'lib/PhpParser/Node/Expr/Cast.php', - 64 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 71 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 72 => 'lib/PhpParser/Node/Expr/Clone_.php', - 73 => 'lib/PhpParser/Node/Expr/Closure.php', - 74 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 75 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 76 => 'lib/PhpParser/Node/Expr/Empty_.php', - 77 => 'lib/PhpParser/Node/Expr/Error.php', - 78 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 79 => 'lib/PhpParser/Node/Expr/Eval_.php', - 80 => 'lib/PhpParser/Node/Expr/Exit_.php', - 81 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 82 => 'lib/PhpParser/Node/Expr/Include_.php', - 83 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 84 => 'lib/PhpParser/Node/Expr/Isset_.php', - 85 => 'lib/PhpParser/Node/Expr/List_.php', - 86 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 87 => 'lib/PhpParser/Node/Expr/New_.php', - 88 => 'lib/PhpParser/Node/Expr/PostDec.php', - 89 => 'lib/PhpParser/Node/Expr/PostInc.php', - 90 => 'lib/PhpParser/Node/Expr/PreDec.php', - 91 => 'lib/PhpParser/Node/Expr/PreInc.php', - 92 => 'lib/PhpParser/Node/Expr/Print_.php', - 93 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 94 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 95 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 96 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 97 => 'lib/PhpParser/Node/Expr/Ternary.php', - 98 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 100 => 'lib/PhpParser/Node/Expr/Variable.php', - 101 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 102 => 'lib/PhpParser/Node/Expr/Yield_.php', - 103 => 'lib/PhpParser/Node/FunctionLike.php', - 104 => 'lib/PhpParser/Node/Name.php', - 105 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 106 => 'lib/PhpParser/Node/Name/Relative.php', - 107 => 'lib/PhpParser/Node/NullableType.php', - 108 => 'lib/PhpParser/Node/Param.php', - 109 => 'lib/PhpParser/Node/Scalar.php', - 110 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 111 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 112 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 113 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 114 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 123 => 'lib/PhpParser/Node/Scalar/String_.php', - 124 => 'lib/PhpParser/Node/Stmt.php', - 125 => 'lib/PhpParser/Node/Stmt/Break_.php', - 126 => 'lib/PhpParser/Node/Stmt/Case_.php', - 127 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 128 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 131 => 'lib/PhpParser/Node/Stmt/Class_.php', - 132 => 'lib/PhpParser/Node/Stmt/Const_.php', - 133 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 134 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 135 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 136 => 'lib/PhpParser/Node/Stmt/Do_.php', - 137 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 138 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 139 => 'lib/PhpParser/Node/Stmt/Else_.php', - 140 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 141 => 'lib/PhpParser/Node/Stmt/For_.php', - 142 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 143 => 'lib/PhpParser/Node/Stmt/Function_.php', - 144 => 'lib/PhpParser/Node/Stmt/Global_.php', - 145 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 146 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 147 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 148 => 'lib/PhpParser/Node/Stmt/If_.php', - 149 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 150 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 151 => 'lib/PhpParser/Node/Stmt/Label.php', - 152 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 153 => 'lib/PhpParser/Node/Stmt/Nop.php', - 154 => 'lib/PhpParser/Node/Stmt/Property.php', - 155 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 156 => 'lib/PhpParser/Node/Stmt/Return_.php', - 157 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 158 => 'lib/PhpParser/Node/Stmt/Static_.php', - 159 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 160 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 161 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 165 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 166 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 167 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 168 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 169 => 'lib/PhpParser/Node/Stmt/Use_.php', - 170 => 'lib/PhpParser/Node/Stmt/While_.php', - 171 => 'lib/PhpParser/NodeDumper.php', - 172 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 173 => 'lib/PhpParser/Parser/Php5.php', - 174 => 'lib/PhpParser/Parser/Php7.php', - 175 => 'lib/PhpParser/ParserAbstract.php', - 176 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 177 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), - 'lib/bootstrap.php' => - array ( - ), -); \ No newline at end of file diff --git a/tests/e2e/resultCache_2.php b/tests/e2e/resultCache_2.php deleted file mode 100644 index 5befe26310..0000000000 --- a/tests/e2e/resultCache_2.php +++ /dev/null @@ -1,2022 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Lexer.php', - 14 => 'lib/PhpParser/Node/Arg.php', - 15 => 'lib/PhpParser/Node/Const_.php', - 16 => 'lib/PhpParser/Node/Expr.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 18 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 19 => 'lib/PhpParser/Node/Expr/Array_.php', - 20 => 'lib/PhpParser/Node/Expr/Assign.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 33 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 34 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 62 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 63 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 64 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 65 => 'lib/PhpParser/Node/Expr/Cast.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 72 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 73 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 74 => 'lib/PhpParser/Node/Expr/Clone_.php', - 75 => 'lib/PhpParser/Node/Expr/Closure.php', - 76 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 77 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 78 => 'lib/PhpParser/Node/Expr/Empty_.php', - 79 => 'lib/PhpParser/Node/Expr/Error.php', - 80 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 81 => 'lib/PhpParser/Node/Expr/Eval_.php', - 82 => 'lib/PhpParser/Node/Expr/Exit_.php', - 83 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 84 => 'lib/PhpParser/Node/Expr/Include_.php', - 85 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 86 => 'lib/PhpParser/Node/Expr/Isset_.php', - 87 => 'lib/PhpParser/Node/Expr/List_.php', - 88 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 89 => 'lib/PhpParser/Node/Expr/New_.php', - 90 => 'lib/PhpParser/Node/Expr/PostDec.php', - 91 => 'lib/PhpParser/Node/Expr/PostInc.php', - 92 => 'lib/PhpParser/Node/Expr/PreDec.php', - 93 => 'lib/PhpParser/Node/Expr/PreInc.php', - 94 => 'lib/PhpParser/Node/Expr/Print_.php', - 95 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 96 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 97 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 98 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 99 => 'lib/PhpParser/Node/Expr/Ternary.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 101 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 102 => 'lib/PhpParser/Node/Expr/Variable.php', - 103 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 104 => 'lib/PhpParser/Node/Expr/Yield_.php', - 105 => 'lib/PhpParser/Node/FunctionLike.php', - 106 => 'lib/PhpParser/Node/Name.php', - 107 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 108 => 'lib/PhpParser/Node/Name/Relative.php', - 109 => 'lib/PhpParser/Node/NullableType.php', - 110 => 'lib/PhpParser/Node/Param.php', - 111 => 'lib/PhpParser/Node/Scalar.php', - 112 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 113 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 114 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 115 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 124 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 125 => 'lib/PhpParser/Node/Scalar/String_.php', - 126 => 'lib/PhpParser/Node/Stmt.php', - 127 => 'lib/PhpParser/Node/Stmt/Break_.php', - 128 => 'lib/PhpParser/Node/Stmt/Case_.php', - 129 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 132 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 133 => 'lib/PhpParser/Node/Stmt/Class_.php', - 134 => 'lib/PhpParser/Node/Stmt/Const_.php', - 135 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 136 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 137 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 138 => 'lib/PhpParser/Node/Stmt/Do_.php', - 139 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 140 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 141 => 'lib/PhpParser/Node/Stmt/Else_.php', - 142 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 143 => 'lib/PhpParser/Node/Stmt/For_.php', - 144 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 145 => 'lib/PhpParser/Node/Stmt/Function_.php', - 146 => 'lib/PhpParser/Node/Stmt/Global_.php', - 147 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 148 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 149 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 150 => 'lib/PhpParser/Node/Stmt/If_.php', - 151 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 152 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 153 => 'lib/PhpParser/Node/Stmt/Label.php', - 154 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 155 => 'lib/PhpParser/Node/Stmt/Nop.php', - 156 => 'lib/PhpParser/Node/Stmt/Property.php', - 157 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 158 => 'lib/PhpParser/Node/Stmt/Return_.php', - 159 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 160 => 'lib/PhpParser/Node/Stmt/Static_.php', - 161 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 162 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 166 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 167 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 168 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 169 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 170 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 171 => 'lib/PhpParser/Node/Stmt/Use_.php', - 172 => 'lib/PhpParser/Node/Stmt/While_.php', - 173 => 'lib/PhpParser/NodeAbstract.php', - 174 => 'lib/PhpParser/NodeDumper.php', - 175 => 'lib/PhpParser/NodeTraverser.php', - 176 => 'lib/PhpParser/NodeTraverserInterface.php', - 177 => 'lib/PhpParser/NodeVisitor.php', - 178 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 179 => 'lib/PhpParser/NodeVisitorAbstract.php', - 180 => 'lib/PhpParser/Parser.php', - 181 => 'lib/PhpParser/Parser/Multiple.php', - 182 => 'lib/PhpParser/Parser/Php5.php', - 183 => 'lib/PhpParser/Parser/Php7.php', - 184 => 'lib/PhpParser/ParserAbstract.php', - 185 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 186 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 187 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Arg.php', - 5 => 'lib/PhpParser/Node/Const_.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 7 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 8 => 'lib/PhpParser/Node/Expr/Array_.php', - 9 => 'lib/PhpParser/Node/Expr/Assign.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 23 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 52 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 53 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 54 => 'lib/PhpParser/Node/Expr/Cast.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 61 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 62 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 63 => 'lib/PhpParser/Node/Expr/Clone_.php', - 64 => 'lib/PhpParser/Node/Expr/Closure.php', - 65 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 66 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 67 => 'lib/PhpParser/Node/Expr/Empty_.php', - 68 => 'lib/PhpParser/Node/Expr/Error.php', - 69 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 70 => 'lib/PhpParser/Node/Expr/Eval_.php', - 71 => 'lib/PhpParser/Node/Expr/Exit_.php', - 72 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 73 => 'lib/PhpParser/Node/Expr/Include_.php', - 74 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 75 => 'lib/PhpParser/Node/Expr/Isset_.php', - 76 => 'lib/PhpParser/Node/Expr/List_.php', - 77 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 78 => 'lib/PhpParser/Node/Expr/New_.php', - 79 => 'lib/PhpParser/Node/Expr/PostDec.php', - 80 => 'lib/PhpParser/Node/Expr/PostInc.php', - 81 => 'lib/PhpParser/Node/Expr/PreDec.php', - 82 => 'lib/PhpParser/Node/Expr/PreInc.php', - 83 => 'lib/PhpParser/Node/Expr/Print_.php', - 84 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 85 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 86 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 87 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 88 => 'lib/PhpParser/Node/Expr/Ternary.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 90 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 91 => 'lib/PhpParser/Node/Expr/Variable.php', - 92 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 93 => 'lib/PhpParser/Node/Expr/Yield_.php', - 94 => 'lib/PhpParser/Node/Param.php', - 95 => 'lib/PhpParser/Node/Scalar.php', - 96 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 97 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 98 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 99 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 108 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 109 => 'lib/PhpParser/Node/Scalar/String_.php', - 110 => 'lib/PhpParser/Node/Stmt/Break_.php', - 111 => 'lib/PhpParser/Node/Stmt/Case_.php', - 112 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 113 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 114 => 'lib/PhpParser/Node/Stmt/Do_.php', - 115 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 116 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 117 => 'lib/PhpParser/Node/Stmt/For_.php', - 118 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 119 => 'lib/PhpParser/Node/Stmt/Global_.php', - 120 => 'lib/PhpParser/Node/Stmt/If_.php', - 121 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 122 => 'lib/PhpParser/Node/Stmt/Return_.php', - 123 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 124 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 125 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 126 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 127 => 'lib/PhpParser/Node/Stmt/While_.php', - 128 => 'lib/PhpParser/NodeDumper.php', - 129 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 130 => 'lib/PhpParser/Parser/Php5.php', - 131 => 'lib/PhpParser/Parser/Php7.php', - 132 => 'lib/PhpParser/ParserAbstract.php', - 133 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 134 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Lexer.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeDumper.php', - 173 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 174 => 'lib/PhpParser/Parser/Php5.php', - 175 => 'lib/PhpParser/Parser/Php7.php', - 176 => 'lib/PhpParser/ParserAbstract.php', - 177 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 178 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), - 'lib/bootstrap.php' => - array ( - ), -); \ No newline at end of file diff --git a/tests/e2e/resultCache_3.php b/tests/e2e/resultCache_3.php deleted file mode 100644 index ccd09a149b..0000000000 --- a/tests/e2e/resultCache_3.php +++ /dev/null @@ -1,2015 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeAbstract.php', - 173 => 'lib/PhpParser/NodeDumper.php', - 174 => 'lib/PhpParser/NodeTraverser.php', - 175 => 'lib/PhpParser/NodeTraverserInterface.php', - 176 => 'lib/PhpParser/NodeVisitor.php', - 177 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 178 => 'lib/PhpParser/NodeVisitorAbstract.php', - 179 => 'lib/PhpParser/Parser.php', - 180 => 'lib/PhpParser/Parser/Multiple.php', - 181 => 'lib/PhpParser/Parser/Php5.php', - 182 => 'lib/PhpParser/Parser/Php7.php', - 183 => 'lib/PhpParser/ParserAbstract.php', - 184 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 185 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 186 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Node/Arg.php', - 4 => 'lib/PhpParser/Node/Const_.php', - 5 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 7 => 'lib/PhpParser/Node/Expr/Array_.php', - 8 => 'lib/PhpParser/Node/Expr/Assign.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 22 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 51 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 52 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 53 => 'lib/PhpParser/Node/Expr/Cast.php', - 54 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 61 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 62 => 'lib/PhpParser/Node/Expr/Clone_.php', - 63 => 'lib/PhpParser/Node/Expr/Closure.php', - 64 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 65 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 66 => 'lib/PhpParser/Node/Expr/Empty_.php', - 67 => 'lib/PhpParser/Node/Expr/Error.php', - 68 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 69 => 'lib/PhpParser/Node/Expr/Eval_.php', - 70 => 'lib/PhpParser/Node/Expr/Exit_.php', - 71 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 72 => 'lib/PhpParser/Node/Expr/Include_.php', - 73 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 74 => 'lib/PhpParser/Node/Expr/Isset_.php', - 75 => 'lib/PhpParser/Node/Expr/List_.php', - 76 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 77 => 'lib/PhpParser/Node/Expr/New_.php', - 78 => 'lib/PhpParser/Node/Expr/PostDec.php', - 79 => 'lib/PhpParser/Node/Expr/PostInc.php', - 80 => 'lib/PhpParser/Node/Expr/PreDec.php', - 81 => 'lib/PhpParser/Node/Expr/PreInc.php', - 82 => 'lib/PhpParser/Node/Expr/Print_.php', - 83 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 84 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 85 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 86 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 87 => 'lib/PhpParser/Node/Expr/Ternary.php', - 88 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 90 => 'lib/PhpParser/Node/Expr/Variable.php', - 91 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 92 => 'lib/PhpParser/Node/Expr/Yield_.php', - 93 => 'lib/PhpParser/Node/Param.php', - 94 => 'lib/PhpParser/Node/Scalar.php', - 95 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 96 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 97 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 98 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 99 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 108 => 'lib/PhpParser/Node/Scalar/String_.php', - 109 => 'lib/PhpParser/Node/Stmt/Break_.php', - 110 => 'lib/PhpParser/Node/Stmt/Case_.php', - 111 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 112 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 113 => 'lib/PhpParser/Node/Stmt/Do_.php', - 114 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 115 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 116 => 'lib/PhpParser/Node/Stmt/For_.php', - 117 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 118 => 'lib/PhpParser/Node/Stmt/Global_.php', - 119 => 'lib/PhpParser/Node/Stmt/If_.php', - 120 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 121 => 'lib/PhpParser/Node/Stmt/Return_.php', - 122 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 123 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 124 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 125 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 126 => 'lib/PhpParser/Node/Stmt/While_.php', - 127 => 'lib/PhpParser/NodeDumper.php', - 128 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 129 => 'lib/PhpParser/Parser/Php5.php', - 130 => 'lib/PhpParser/Parser/Php7.php', - 131 => 'lib/PhpParser/ParserAbstract.php', - 132 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 133 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Node/Arg.php', - 13 => 'lib/PhpParser/Node/Const_.php', - 14 => 'lib/PhpParser/Node/Expr.php', - 15 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 17 => 'lib/PhpParser/Node/Expr/Array_.php', - 18 => 'lib/PhpParser/Node/Expr/Assign.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 32 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 61 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 62 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 63 => 'lib/PhpParser/Node/Expr/Cast.php', - 64 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 71 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 72 => 'lib/PhpParser/Node/Expr/Clone_.php', - 73 => 'lib/PhpParser/Node/Expr/Closure.php', - 74 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 75 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 76 => 'lib/PhpParser/Node/Expr/Empty_.php', - 77 => 'lib/PhpParser/Node/Expr/Error.php', - 78 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 79 => 'lib/PhpParser/Node/Expr/Eval_.php', - 80 => 'lib/PhpParser/Node/Expr/Exit_.php', - 81 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 82 => 'lib/PhpParser/Node/Expr/Include_.php', - 83 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 84 => 'lib/PhpParser/Node/Expr/Isset_.php', - 85 => 'lib/PhpParser/Node/Expr/List_.php', - 86 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 87 => 'lib/PhpParser/Node/Expr/New_.php', - 88 => 'lib/PhpParser/Node/Expr/PostDec.php', - 89 => 'lib/PhpParser/Node/Expr/PostInc.php', - 90 => 'lib/PhpParser/Node/Expr/PreDec.php', - 91 => 'lib/PhpParser/Node/Expr/PreInc.php', - 92 => 'lib/PhpParser/Node/Expr/Print_.php', - 93 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 94 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 95 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 96 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 97 => 'lib/PhpParser/Node/Expr/Ternary.php', - 98 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 100 => 'lib/PhpParser/Node/Expr/Variable.php', - 101 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 102 => 'lib/PhpParser/Node/Expr/Yield_.php', - 103 => 'lib/PhpParser/Node/FunctionLike.php', - 104 => 'lib/PhpParser/Node/Name.php', - 105 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 106 => 'lib/PhpParser/Node/Name/Relative.php', - 107 => 'lib/PhpParser/Node/NullableType.php', - 108 => 'lib/PhpParser/Node/Param.php', - 109 => 'lib/PhpParser/Node/Scalar.php', - 110 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 111 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 112 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 113 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 114 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 123 => 'lib/PhpParser/Node/Scalar/String_.php', - 124 => 'lib/PhpParser/Node/Stmt.php', - 125 => 'lib/PhpParser/Node/Stmt/Break_.php', - 126 => 'lib/PhpParser/Node/Stmt/Case_.php', - 127 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 128 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 131 => 'lib/PhpParser/Node/Stmt/Class_.php', - 132 => 'lib/PhpParser/Node/Stmt/Const_.php', - 133 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 134 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 135 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 136 => 'lib/PhpParser/Node/Stmt/Do_.php', - 137 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 138 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 139 => 'lib/PhpParser/Node/Stmt/Else_.php', - 140 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 141 => 'lib/PhpParser/Node/Stmt/For_.php', - 142 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 143 => 'lib/PhpParser/Node/Stmt/Function_.php', - 144 => 'lib/PhpParser/Node/Stmt/Global_.php', - 145 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 146 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 147 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 148 => 'lib/PhpParser/Node/Stmt/If_.php', - 149 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 150 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 151 => 'lib/PhpParser/Node/Stmt/Label.php', - 152 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 153 => 'lib/PhpParser/Node/Stmt/Nop.php', - 154 => 'lib/PhpParser/Node/Stmt/Property.php', - 155 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 156 => 'lib/PhpParser/Node/Stmt/Return_.php', - 157 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 158 => 'lib/PhpParser/Node/Stmt/Static_.php', - 159 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 160 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 161 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 165 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 166 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 167 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 168 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 169 => 'lib/PhpParser/Node/Stmt/Use_.php', - 170 => 'lib/PhpParser/Node/Stmt/While_.php', - 171 => 'lib/PhpParser/NodeDumper.php', - 172 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 173 => 'lib/PhpParser/Parser/Php5.php', - 174 => 'lib/PhpParser/Parser/Php7.php', - 175 => 'lib/PhpParser/ParserAbstract.php', - 176 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 177 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), -); diff --git a/tests/generate-reflection-test.php b/tests/generate-reflection-test.php new file mode 100644 index 0000000000..eba37b5454 --- /dev/null +++ b/tests/generate-reflection-test.php @@ -0,0 +1,10 @@ + - - - - This Schema file defines the rules by which the XML configuration file of PHPUnit 9.5 may be structured. - - - - - - Root Element - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The main type specifying the document structure - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -